上两遍已经描述了动态多数据源的原理和基础实现了,前面的数据源配置都是从application.yml中配置多数据源的,这里再拓展补充一下其他场景,如何读取数据源不从application.yml中配置,实现从数据库中读取数据源配置并动态切换数据源。

一、回顾上篇的动态多数据源配置

上篇:springboot动态多数据源配置和使用(二)

  1. 继承AbstractRoutingDataSource,重写抽象方法determineCurrentLookupKey()
/**
 * 多数据源
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicContextHolder.peek();
    }

}
  1. 注入spring容器
    @Bean
    public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置多个数据源的map
        dynamicDataSource.setTargetDataSources(getDynamicDataSource())
        //默认数据源
        DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        return dynamicDataSource;
    }

    private Map<Object, Object> getDynamicDataSource(){
        Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
        Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
        dataSourcePropertiesMap.forEach((k, v) -> {
            DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
            targetDataSources.put(k, druidDataSource);
        });

        return targetDataSources;
    }

  1. 可以看到多数据源配置是从application.yml读取出来,然后设置到DynamicDataSource对象里的targetDataSources属性
    dynamicDataSource.setTargetDataSources();

分析

从上面分析可以知道,重要的是targetDataSources这个存放多数据源的map属性。
那么我们只要把targetDataSources这个map由配置文件获取创建dataSource然后放入map改写成由数据读读取出来的配置,再创建dataSource再放入targetDataSources这个map变量就可以实现我们想要的功能了。

二、从数据库获取配置创建数据源

这一步说难也不难,就把数据库的配置保存在数据库的表里面,在切面类切换数据源时读取数据库的配置,然后创建数据源,把创建的数据源通过put方法放入targetDataSources这个map即可,最后在切面类DynamicContextHolder.push(key)改变数据源

但是这样子就很没效率,每次都从数据库读取配置,然后创建dataSource数据源。所以实际上我们是懒加载的模式,再用一个数据源缓存池pool来保存dataSource,如果缓存有了dataSource就不再从数据库读取了,直接从数据源缓存池的pool来获取数据源。

具体实现

1.创建表
CREATE TABLE `oa_quick_knife_datasource` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `datasource_name` varchar(20) DEFAULT '' COMMENT '数据源名称',
  `datasource_url` varchar(200) DEFAULT '' COMMENT '数据源url',
  `datasource_account` varchar(50) DEFAULT '' COMMENT '数据源帐号',
  `datasource_password` varchar(64) DEFAULT '' COMMENT '数据源密码',
  `remark` varchar(200) DEFAULT NULL COMMENT '备注',
  `is_show_type` tinyint(1) DEFAULT NULL COMMENT '数据源可见类型(1-全部人可见,2-部分人可见)',
  `datasource_type` tinyint(1) DEFAULT NULL COMMENT '默认mysql,暂时只支持mysql',
  `update_name` varchar(20) DEFAULT '' COMMENT '更新人',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `create_code` int(11) unsigned DEFAULT '0' COMMENT '创建人工号',
  `create_name` varchar(20) DEFAULT '' COMMENT '创建人',
  `update_code` int(11) unsigned DEFAULT '0' COMMENT '更新人工号',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `deleted_flag` tinyint(1) DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据源 ';

注:datasource_password这个字段我们不要明文保存数据库密码,我们加密后再放入这个字段里面

2.数据源缓存池

数据源缓存池的类代码

/**
 * 数据源缓存池
 */
public class DataSourceCachePool {
    /** 数据源连接池缓存【本地 class缓存 - 不支持分布式】 */
    private static Map<String, DruidDataSource> dbSources = new HashMap<>();
    private static RedisTemplate<String, Object> redisTemplate;

    private static RedisTemplate<String, Object> getRedisTemplate() {
        if (redisTemplate == null) {
            redisTemplate = (RedisTemplate<String, Object>) SpringContextUtils.getBean("redisTemplate");
        }
        return redisTemplate;
    }

    /**
     * 获取多数据源缓存
     *
     * @param dbKey
     * @return
     */
    public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
        String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
        if (getRedisTemplate().hasKey(redisCacheKey)) {
            String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
            return  JSON.parseObject(model,DynamicDataSourceModel.class);
        }
        DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
        DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
        try{
            dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
        }catch (Exception e){
            throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
        }

        if (dbSource != null) {
            getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
        }
        return dbSource;
    }

    public static DruidDataSource getCacheBasicDataSource(String dbKey) {
        return dbSources.get(dbKey);
    }

    /**
     * put 数据源缓存
     *
     * @param dbKey
     * @param db
     */
    public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
        dbSources.put(dbKey, db);
    }

    /**
     * 清空数据源缓存
     */
    public static void cleanAllCache() {
        //关闭数据源连接
        for(Map.Entry<String, DruidDataSource> entry : dbSources.entrySet()){
            String dbkey = entry.getKey();
            DruidDataSource druidDataSource = entry.getValue();
            if(druidDataSource!=null && druidDataSource.isEnable()){
                druidDataSource.close();
            }
            //清空redis缓存
            getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbkey);
        }
        //清空缓存
        dbSources.clear();
    }

    public static void removeCache(String dbKey) {
        //关闭数据源连接
        DruidDataSource druidDataSource = dbSources.get(dbKey);
        if(druidDataSource!=null && druidDataSource.isEnable()){
            druidDataSource.close();
        }
        //清空redis缓存
        getRedisTemplate().delete(ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey);
        //清空缓存
        dbSources.remove(dbKey);
    }

}

上面的数据源缓存池主要代码是下面getCacheDynamicDataSourceModel方法的这段

这个方法的逻辑是先从redis缓存数据源配置,redis没有则从数据库获取,以及获取的配置的数据库密码是加密的,所以这里还要再解密

    /**
     * 获取多数据源缓存配置
     *
     * @param dbKey
     * @return
     */
    public static DynamicDataSourceModel getCacheDynamicDataSourceModel(String dbKey) {
        String redisCacheKey = ConfigConstant.SYS_DYNAMICDB_CACHE + dbKey;
        if (getRedisTemplate().hasKey(redisCacheKey)) {
            String model = (String)getRedisTemplate().opsForValue().get(redisCacheKey);
            return  JSON.parseObject(model,DynamicDataSourceModel.class);
        }
        DatasourceDao datasourceDao = (DatasourceDao)SpringContextUtils.getBean("datasourceDao");
        DynamicDataSourceModel dbSource = datasourceDao.getDynamicDbSourceByCode(dbKey);
        try{
            dbSource.setDbPassword(AesUtil.decryptBySalt(dbSource.getDbPassword(),dbSource.getId()));
        }catch (Exception e){
            throw new RRException("动态数据源密钥解密失败,dbKey:"+dbKey);
        }

        if (dbSource != null) {
            getRedisTemplate().opsForValue().set(redisCacheKey, JSONObject.toJSONString(dbSource));
        }
        return dbSource;
    }

还有一个重要的方法,把数据源放入缓存池的dbSource这个map属性里面

    /**
     * put 数据源缓存
     *
     * @param dbKey
     * @param db
     */
    public static void putCacheBasicDataSource(String dbKey, DruidDataSource db) {
        dbSources.put(dbKey, db);
    }
3.再写一个工具类DynamicDBUtil 操作数据源缓存池

这个类的核心方法是getDbSourceByDbKey(),先判断缓存池有没有对应key的数据源,没有则读取数据源配置(先从redis读配置,没有再从数据库读配置),根据配置创建DruidDataSource数据源,再把数据源放入缓存池

getDbSourceByDbKey这个方法的dbKey是指能根据这个key找到数据库对应的记录,这里指该表的id

/**
 * Spring JDBC 实时数据库访问
 *
 */
@Slf4j
public class DynamicDBUtil {

    /**
     * 通过 dbKey ,获取数据源
     *
     * @param dbKey
     * @return
     */
    public static DruidDataSource getDbSourceByDbKey(final String dbKey) {

        //先判断缓存中是否存在数据库链接
        DruidDataSource cacheDbSource = DataSourceCachePool.getCacheBasicDataSource(dbKey);
        if (cacheDbSource != null && !cacheDbSource.isClosed()) {
            log.debug("--------getDbSourceBydbKey------------------从缓存中获取DB连接-------------------");
            return cacheDbSource;
        } else {
            //获取多数据源配置
            DynamicDataSourceModel dbSource = DataSourceCachePool.getCacheDynamicDataSourceModel(dbKey);
            DruidDataSource dataSource = getJdbcDataSource(dbSource);
            if(dataSource!=null && dataSource.isEnable()){
                DataSourceCachePool.putCacheBasicDataSource(dbKey, dataSource);
            }else{
                throw new RRException("动态数据源连接失败,dbKey:"+dbKey);
            }
            log.info("--------getDbSourceBydbKey------------------创建DB数据库连接-------------------");
            return dataSource;
        }
    }

    /**
     * 获取数据源【最底层方法,不要随便调用】
     *
     * @param dbSource
     * @return
     */
    private static DruidDataSource getJdbcDataSource(final DynamicDataSourceModel dbSource) {
        DruidDataSource dataSource = new DruidDataSource();

        String driverClassName = dbSource.getDbDriver();
        String url = dbSource.getDbUrl();
        String dbUser = dbSource.getDbUsername();
        String dbPassword = dbSource.getDbPassword();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        //dataSource.setValidationQuery("SELECT 1 FROM DUAL");
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(false);
        dataSource.setTestOnReturn(false);
        dataSource.setBreakAfterAcquireFailure(true);
        dataSource.setConnectionErrorRetryAttempts(0);
        dataSource.setUsername(dbUser);
        dataSource.setMaxWait(60000);
        dataSource.setPassword(dbPassword);

        log.info("******************************************");
        log.info("*                                        *");
        log.info("*====【"+dbSource.getCode()+"】=====Druid连接池已启用 ====*");
        log.info("*                                        *");
        log.info("******************************************");
        return dataSource;
    }

    /**
     * 关闭数据库连接池
     *
     * @param dbKey
     * @return
     */
    public static void closeDbKey(final String dbKey) {
        DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
        try {
            if (dataSource != null && !dataSource.isClosed()) {
                dataSource.getConnection().commit();
                dataSource.getConnection().close();
                dataSource.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }


    private static JdbcTemplate getJdbcTemplate(String dbKey) {
        DruidDataSource dataSource = getDbSourceByDbKey(dbKey);
        return new JdbcTemplate(dataSource);
    }

    /**
     * 获取连接
     * @param url
     * @param username
     * @param password
     * @param driverName
     * @return
     */
    public static Connection getConn(String url,String username,String password,String driverName) {
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            throw new RRException("无法连接,问题:"+e.getMessage(), e);
        }

        return conn;
    }

    /**
     * 关闭数据库连接
     * @param
     */
    public static void closeConnection(Connection conn) {
        try {
            if(conn!=null){
                conn.close();
            }
        } catch (SQLException e) {
            throw new RRException("close connection failure", e);
        }
    }

}

4. 继承继承AbstractRoutingDataSource,重写抽象方法determineCurrentLookupKey()

这里比上一篇的DynamicDataSource新增了targetDataSources静态变量和setDataSource()静态方法。

targetDataSources这个属性用于存放多数据源

/**
 * 多数据源
 *
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    public static Map<Object, Object> targetDataSources = new ConcurrentHashMap<>(10);

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicContextHolder.peek();
    }

    public static void setDataSource(String dbKey) throws Exception{
        if(!DynamicDataSource.targetDataSources.containsKey(dbKey)){
            DruidDataSource dataSource = DynamicDBUtil.getDbSourceByDbKey(dbKey);
            DynamicDataSource.targetDataSources.put(dbKey,dataSource);
        }
        //切换动态多数据源的dbKey
        DynamicContextHolder.push(dbKey);
        DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtils.getBean("dynamicDataSource");
        //使得修改后的targetDataSources生效
        dynamicDataSource.afterPropertiesSet();
    }

}
5. 数据源配置类

下面代码通过dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources)把值引用赋值给dynamicDataSource对象(即指向同一块内存,修改了静态变量targetDataSources,就相当于修改了dynamicDataSource对象里面的targetDataSources属性)

@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
    @Autowired
    private DynamicDataSourceProperties properties;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DruidDataSource defaultDataSource(DataSourceProperties dataSourceProperties) {
        //默认数据源,通过配置获取创建
        DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
        return defaultDataSource;
    }

    @Bean
    @Primary
    @DependsOn({"defaultDataSource"})
    public DynamicDataSource dynamicDataSource(DruidDataSource defaultDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置targetDataSources(通过数据库配置获取,首次创建没有数据源)
        dynamicDataSource.setTargetDataSources(DynamicDataSource.targetDataSources);

        //默认数据源
        dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
        return dynamicDataSource;
    }

}
6. 再写一个注解以及实现这个注解的切面类就可以了
/**
 * 多数据源注解
 *
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
    String value() default "";
}
/**
 * 多数据源,切面处理类
 *
 */
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(io.renren.datasource.annotation.DataSource) " +
            "|| @within(io.renren.datasource.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class targetClass = point.getTarget().getClass();
        Method method = signature.getMethod();

        DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
        DataSource methodDataSource = method.getAnnotation(DataSource.class);
        if(targetDataSource != null || methodDataSource != null){
            String value;
            if(methodDataSource != null){
                value = methodDataSource.value();
            }else {
                value = targetDataSource.value();
            }
            //根据dbKey动态设置数据源
            DynamicDataSource.setDataSource(dbKey);
            logger.debug("set datasource is {}", value);
        }

        try {
            return point.proceed();
        } finally {
            DynamicContextHolder.poll();
            logger.debug("clean datasource");
        }
    }
}

到这一步就已完成了,然后把DataSource注解加到service的类或方法上,即可实现操作指定的多数据源。

三、通过接口的入参来指定数据源

上面的注解DataSource的value是写死在代码里面的,但是我们有这样的需求,前端根据接口入参来操作指定数据源的数据。

所以我们在上面的基础上,再改造一下
再写两个注解

/**
 * 多数据源注解-注解数据源的dbKey
 *
 * @author ZhangXinLin
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DbKey {
}

这个自定义注解DbKey,是作用在参数上的,标志该参数是用来指定数据源的dbKey

/**
 * 多数据源注解
 *
 * @author ZhangXinLin
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DynamicDataSource {

}

DynamicDataSource的自定义注解是用在controller的方法上

DynamicDataSource注解的切面类

这个切面类是根据方法的入参dbKey来动态切换数据源,核心代码是调用这行代码
//根据dbKey动态设置数据源
DynamicDataSource.setDataSource(dbKey);

/**
 * @Description: 动态加载多数据源(启动后加载)
 **/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DynamicDataSourceAspect {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(io.renren.datasource.annotation.DynamicDataSource) " +
            "|| @within(io.renren.datasource.annotation.DynamicDataSource)")
    public void dynamicdataSourcePointCut() {

    }

    @Around("dynamicdataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //获取参数,根据参数获取数据源
        String dbKey = getDbKey(point);

        if( dbKey != null){
            //根据dbKey动态设置数据源
            DynamicDataSource.setDataSource(dbKey);
        }
        try {
            return point.proceed();
        } finally {
            DynamicContextHolder.poll();
            logger.debug("clean datasource");
        }
    }


    /**
     * 根据@DbKey注解获取数据源的dbKey
     * @param point
     * @return
     */
    private String getDbKey(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Object[] args = point.getArgs();
        String value = null;
        //参数注解,1维是参数,2维是注解
        Annotation[][] annotations = method.getParameterAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            Object param = args[i];
            Annotation[] paramAnn = annotations[i];
            //参数为空,直接下一个参数
            if (param == null || paramAnn.length == 0) {
                continue;
            }
            for (Annotation pAnnotation : paramAnn) {
                if (pAnnotation.annotationType().equals(DbKey.class)) {
                    value =  param.toString();
                    break;
                }
            }
        }
        return value;
    }
}

然后在controller的方法上加上注解@DynamicDataSource,以及入参加上注解@Dbkey

    /**
     * 查看数据源的所有表列表
     * @param id
     * @return
     */
    @DynamicDataSource
    @RequestMapping("/getTableList/{id}")
    public R getTableList(@PathVariable("id") @DbKey Integer id){
        List<Map<String, Object>> list = datasourceService.queryTableList(id);
        return R.ok().put("list", list);
    }
看看实际效果
  1. 首先我们表的数据源的增删改写一个页面
    在这里插入图片描述

在这里插入图片描述

  1. 在页面调用getTableList接口来切换数据源的数据

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
可以看到,前端页面选择不同的数据库,后端接口就会根据dbKey的入参来动态切换数据源,从而查询出不同数据源的表名列表



源码

源码在一个还没有写完的快速开发平台的项目里面(功能可以在线编写模版,线上配置数据源,不用改代码就可以编写开发模版,生成不同系统的基础代码);
这个项目还没写完,后面写完也会开源出来,所以这里的源码暂时没有

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐