• 项目要实现多数据源动态切换,咋搞?


    文章首发于公众号:编程大道

    在做项目的时候,几乎都会用到数据库,很多时候就只连一个数据库,但是有时候我们需要一个项目操作多个数据库,不同的业务功能产生的数据存到不同的数据库,那怎么来实现数据源的动态、灵活的切换呢?今天我们就来实现这个功能。

    前期准备工作

    我们需要有一台联网的电脑(用于maven自动下载依赖),并且电脑安装JDK 8、IDEA、MySQL数据库、maven,首先创建一个springboot项目(SSM也行)。springboot版本和SSM版本的代码都已经放到码云托管,表结构SQL也有,感兴趣的可以去下载https://gitee.com/itwalking/springboot-dynamic-datasourcehttps://gitee.com/itwalking/ssm-dynamic-datasource

    实现思路

    首先讲一下我们的实现思路,平时我们做项目,都会用到spring来集成我们的数据源,连接mysqlOracle数据库,通过暴露出DataSource相关的接口,然后不同的数据库都可以集成过来,我们只需要配置数据源的四大参数即可,这是我们往常的做法。而如果使用动态数据源的话,Spring也为我们提供了相应的扩展点,那就是AbstractRoutingDataSource抽象类,它同样是jdbcDataSource接口的实现类。

    代码

    废话不多说,我们直接上代码。
    创建我们自己的数据源DynamicDataSource继承AbstractRoutingDataSource,实现它的抽象方法determineCurrentLookupKey,这个方法其实就是实现动态选择数据源的关键,通过这个方法返回的对象关联到我们的数据源。(已对这个类做了一点优化,具体代码在码云托管https://gitee.com/itwalking/springboot-dynamic-datasourcehttps://gitee.com/itwalking/ssm-dynamic-datasource

    package com.walking.db;
     
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
     
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
     
    /**
     * @author walking
     * 公众号:编程大道
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
        /**
         * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
         * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
         */
        private static final ThreadLocal<DataSourceName> dataSourceName = new ThreadLocal<DataSourceName>();
        /**
         * 支持以包名的粒度选择数据源
         */
        private static final Map<String,DataSourceName> packageDataSource = new HashMap<>();
     
        public DynamicDataSource(DataSource firstDataSource, Map<Object, Object> targetDataSources) {
            setDefaultTargetDataSource(firstDataSource);
            setTargetDataSources(targetDataSources);
            afterPropertiesSet();
        }
     
        /**
         * 获取与线程上下文绑定的数据源名称(存储在ThreadLocal中)
         * @return 返回数据源名称
         */
        @Override
        protected Object determineCurrentLookupKey() {
            DataSourceName dsName = dataSourceName.get();
            dataSourceName.remove();
            return dsName;
        }
        public static void setDataSourceName(DataSourceName dataSource){
            dataSourceName.set(dataSource);
        }
        public static void usePackageDatasourceKey(String pkName) {
            dataSourceName.set(packageDataSource.get(pkName));
        }
        public Map<String,DataSourceName> getPackageDatasource(){
            return packageDataSource;
        }
        public void setPackageDatasource(Map<String,DataSourceName> packageDatasource){
            this.packageDataSource.putAll(packageDatasource);
        }
    }

    DynamicDataSource中有一个ThreadLocal用来保存我们当前选择的数据源名称,代码中的注释写的很清楚了。其中ThreadLocal的泛型是DataSourceNameDataSourceName是我们自己定义的一个枚举类,用于定义我们的数据源名称,我这里拿两个数据源做演示,并命名为FIRSTSECOND

    package com.walking.db;
    /**
     * @author walking
     * 公众号:编程大道
     */
    public enum DataSourceName {
        FIRST, SECOND;
    }

    然后自定义一个注解,用于标注我们操作数据库时选择哪个数据源,很简单只有一个name属性,默认是 DataSourceName.FIRST

    @Target({ElementType.PACKAGE,ElementType.TYPE,ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CurDataSource {
     
        /**
         * name of DataSource
         * @return
         */
        DataSourceName value() default DataSourceName.FIRST;
     
    }

    有了这些还不够,我们还需要根据我们的注解里的name属性动态的去修改 DynamicDataSource 中 ThreadLocal 中保存的数据库名称,每次执行SQL前都要修改数据源,这样才能达到修改数据源的目的。那很显然我们就需要spring AOP来完成这个操作了。

    如下,DynamicDataSourceAspect 是我们定义的一个切面类,同时也定了三个切点,分别去切方法上带@CurDataSource注解的方法,类上带@CurDataSource注解的类,以及按包名去切。这样,我们的动态数据源就支持方法级别的、类级别的、包级别的动态配置了。

    package com.walking.aaspect;
     
    import com.walking.db.CurDataSource;
    import com.walking.db.DataSourceName;
    import com.walking.db.DynamicDataSource;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
     
    import java.lang.reflect.Method;
    import java.util.Objects;
     
    /**
     * 动态数据源切面类
     * 被切中的,则先判断方法上是否有CurDataSource注解
     * 然后判断方法所属类上是否有CurDataSource注解
     * 其次判断是否配置了包级别的数据源
     *
     * 优先级为方法、类、包
     * 若同时配置则优先按方法上的
     *
     * @author walking
     * 公众号:编程大道
     */
    @Slf4j
    @Aspect
    @Component
    public class DynamicDataSourceAspect {
        // pointCut
        @Pointcut("@annotation(com.walking.db.CurDataSource)")
        public void choseDatasourceByAnnotation() {
        }
        @Pointcut("@within(com.walking.db.CurDataSource)")
        public void choseDatasourceByClass() {
        }
        @Pointcut("execution(* com.walking.service3..*(..))")
        public void choseDatasourceByPackage() {
        }
     
        @Around("choseDatasourceByAnnotation() || choseDatasourceByClass() || choseDatasourceByPackage()")
        public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("进入AOP环绕通知");
            Signature signature = joinPoint.getSignature();
            DataSourceName datasourceName = getDatasourceKey(signature);
            if (!Objects.isNull(datasourceName)) {
                DynamicDataSource.setDataSourceName(datasourceName);
            }
            return joinPoint.proceed();
        }
        private DataSourceName getDatasourceKey(Signature signature) {
            if (signature == null) {
                return null;
            } else {
                if (signature instanceof MethodSignature) {
                    MethodSignature methodSignature = (MethodSignature) signature;
                    Method method = methodSignature.getMethod();
                    if (method.isAnnotationPresent(CurDataSource.class)) {
                        return this.dsSettingInMethod(method);
                    }
                    Class<?> declaringClass = method.getDeclaringClass();
                    if (declaringClass.isAnnotationPresent(CurDataSource.class)) {
                        return this.dsSettingInConstructor(declaringClass);
                    }
                    Package aPackage = declaringClass.getPackage();
                    this.dsSettingInPackage(aPackage);
                }
                return null;
            }
        }
        private DataSourceName dsSettingInConstructor(Class<?> declaringClass) {
            CurDataSource dataSource = declaringClass.getAnnotation(CurDataSource.class);
            return dataSource.value();
        }
        private DataSourceName dsSettingInMethod(Method method) {
            CurDataSource dataSource = method.getAnnotation(CurDataSource.class);
            return dataSource.value();
        }
        private void dsSettingInPackage(Package pkg) {
            DynamicDataSource.usePackageDatasourceKey(pkg.getName());
        }
    }

    仔细看一下这个切面类的环绕通知这个方法的逻辑,可以发现,我们首先看的是方法上的注解,然后再看类上的注解,最后看是否配置了包级别数据源。

    基本上,该有的类我们都写完了,剩下就是验证。

    验证之前我们还需要进行一些配置。

    配置多数据源

    这里,我们使用的是阿里的Druid数据源,用springboot自带的也行。我们可以看到在Druid:配置下,原本直接就配置url、name这些参数,我们新增了一级分别是first和second,用于配置多个数据源

    server:
      port: 9966
      servlet:
        context-path: /walking
    spring:
      mvc:
        log-request-details: false
      application:
        name: walking
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        druid:
          first:
            url: jdbc:mysql://localhost:3306/walking_mybatis?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
            username: root
            password: 123456ppzy,
          second:
            url: jdbc:mysql://localhost:3306/walking_mybatis2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
            username: root
            password: 123456ppzy,
     
    mybatis:
      mapper-locations: classpath:mapper/*.xml

    然后javaconfig配置类。
    配置了三个bean,前两个是数据源的bean,使用@ConfigurationProperties注解,让springboot帮我们去配置文件读取指定前缀的配置,这样我们刚才配的两个数据源参数就区分开了。
    然后第三个bean是我们配置的叫做dataSource的bean,用于覆盖spring默认的DataSource,在这个bean中,我们把所有的数据源注入进去,这里我们有两个,命名为FIRST和SECOND(DataSourceName枚举类),以及我们要配置的包级别的数据源,然后调用构造函数创建DynamicDataSource我们的动态数据源。并指明了默认的数据源。

    package com.walking.configuration;
     
    import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
    import com.walking.db.DataSourceName;
    import com.walking.db.DynamicDataSource;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
     
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
     
    /**
     * @author walking
     * 公众号:编程大道
     */
    @Configuration
    public class DynamicDataSourceConfig {
        @Bean
        @ConfigurationProperties("spring.datasource.druid.first")
        public DataSource firstDataSource() {
            return DruidDataSourceBuilder.create().build();
        }
        @Bean
        @ConfigurationProperties("spring.datasource.druid.second")
        public DataSource secondDataSource() {
            return DruidDataSourceBuilder.create().build();
        }
        @Bean
        @Primary
        public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
            Map<Object, Object> targetDataSources = new HashMap<>(2);
            targetDataSources.put(DataSourceName.FIRST, firstDataSource);
            targetDataSources.put(DataSourceName.SECOND, secondDataSource);
     
            //配置包级别的数据源
            Map<String, DataSourceName> packageDataSource = new HashMap<>();
            packageDataSource.put("com.walking.service3", DataSourceName.SECOND);
     
            DynamicDataSource dynamicDataSource new DynamicDataSource(firstDataSource, targetDataSources);
            dynamicDataSource.setPackageDatasource(packageDataSource);
            dynamicDataSource.afterPropertiesSet();
            return dynamicDataSource;
        }
    }

    然后就是我们的启动类了,我们需要禁用掉spring的自动配置数据源,和Druid的自动配置数据源,使用我们自定义的动态数据源。

    @EnableAspectJAutoProxy
    //关掉数据源自动配置
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
            DruidDataSourceAutoConfigure.class})
    //使用Import注解导入我们自己的数据源配置 或在DynamicDataSourceConfig上加Configuration注解
    //@Import({DynamicDataSourceConfig.class})
    @MapperScan(basePackages = "com.walking.dao")
    public class App {
        public static void main(String[] args) {
            SpringApplication.run(App.class, args);
        }
    }

    操作Mybatis我就不多说了,这里我在两个数据库(walking_mybatis和walking_mybatis2)里创建了相同的user表,我们测试的时候观察插入到哪个表就OK了。

    项目整体结构

    测试

    我们在save_1上添加注解指明使用SECOND,在save_2则没有,UserService1类上也没用注解,同样的,在配置类里也没配置UserService1的包名,那么save_2将会使用默认的数据源那就是FIRST

    controller

    运行,访问http://localhost:9966/walking/test01
    日志输出

    查看数据库则第二个数据库新增一条数据。
    完整代码我已上传gitee码云,详细的测试都在这三个service包下和test包下,感兴趣的可以去下载代码看看。

    实现动态数据源切换就是这么简单。下次我们看一下动态数据源的原理。

    总结一下

    1、继承AbstractRoutingDataSource实现多数据源及默认数据源的配置
    2、注解+AOP,实现动态修改数据源的逻辑
    3、排除spring和Druid(如果引入了第三方数据库连接池)默认的自动配置数据源

    动手操作下一下,SQL和项目都已上传。

    欢迎关注公众号:编程大道

  • 相关阅读:
    Ext.js给form加背景图片
    Linux安装Scala
    idea 无法创建Scala class 选项解决办法汇总
    idea 无法创建Scala class 选项解决办法汇总
    i++和++i的区别,及其线程安全问题
    java面试题
    大数据学习——scala入门程序
    大数据学习——spark安装
    大数据学习——kafka+storm+hdfs整合
    大数据学习——日志监控告警系统
  • 原文地址:https://www.cnblogs.com/ibigboy/p/13839637.html
Copyright © 2020-2023  润新知