美文网首页
SpringBoot(三)动态数据源切换

SpringBoot(三)动态数据源切换

作者: TiaNa_na | 来源:发表于2020-02-05 15:08 被阅读0次

      最近有一个项目国际化的需求,解决方案一般是这样的:WEB网站国际化的一种解决方案
      简单来说,国际化一方面需要配置静态文字,另一方面需要管理动态数据。静态文字国际化可参考:SpringBoot项目国际化;;SpringBoot的国际化错误信息返回,下文我们主要讲的就是动态数据国际化。
      实现思路:利用AOP或拦截器实现数据库动态切换。

    动态数据源切换时会遇到事务的问题,这个问题暂时还未考虑,下文也不涉及,这个坑留着以后再填。。(主要是太懒了)

    一、准备工作

    • 创建多个数据库,数据库名分别为dev,dev_hk,dev_en,每个数据库的表名是一样的。
    • 添加依赖pom.xml,下面将利用AOP实现数据源动态切换,所以要引入aop的依赖。
        <!-- 引入aop -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    
    • application.yml配置文件中配置数据源;
    server:
      port: 8081
    
    spring:
      messages:
          basename: i18n/messages
          encoding: UTF-8
    
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        cn:
          url: jdbc:mysql://localhost:3306/dev?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
          username: test
          password: 123456
        hk:
          url: jdbc:mysql://localhost:3306/dev_hk?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
          username: test
          password: 123456
        en:
          url: jdbc:mysql://localhost:3306/dev_en?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
          username: test
          password: 123456
    
       # 配置连接池
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          initialSize: 5
          minIdle: 5
          maxActive: 20
          maxWait: 60000
          # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
          timeBetweenEvictionRunsMillis: 60000
          # 配置一个连接在池中最小生存的时间,单位是毫秒
          minEvictableIdleTimeMillis: 30000
          validationQuery: SELECT 1 FROM DUAL
          testWhileIdle: true
          testOnBorrow: false
          testOnReturn: false
          poolPreparedStatements: true
          maxPoolPreparedStatementPerConnectionSize: 20
          # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
          filters: stat,wall,log4j
          # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
          connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
          # 自定义属性,用于druid监控界面的账号、密码配置
          servlet:
            username: test
            password: 123456
      http:
        log-request-details: true
    
      servlet:
        multipart:
          max-file-size: 20MB
          max-request-size: 30MB
          file-size-threshold: 0
          enabled: true
    
    mybatis:
      mapper-locations: classpath:mapper/*Mapper.xml
      config-location: classpath:mybatis-config.xml
    
    mybatis-plus:
      global-config:
        db-config:
          logic-delete-value: 0 # 逻辑已删除值(默认为 0)
          logic-not-delete-value: 1 # 逻辑未删除值(默认为 1)
    

    二、准备工作

    • 数据源配置类,获取取application中数据源的配置,分别构建三个数据源。
    public class DynamicDataSourceConfig {
    
        /**
         * 简体中文数据库   application.yml spring.datasource.cn 配置信息
         *
         * @return DataSource
         */
        @Bean(name = "cnDataSource")
        @ConfigurationProperties("spring.datasource.cn")
        public DataSource cnDataSource() {
            return new DruidDataSource();
        }
    
        /**
         * 繁体中文数据库  application.yml spring.datasource.hk 配置信息
         *
         * @return DataSource
         */
        @Bean(name = "hkDataSource")
        @ConfigurationProperties("spring.datasource.hk")
        public DataSource hkDataSource() {
            return new DruidDataSource();
        }
    
        /**
         * 英文数据库  application.yml  spring.datasource.en 配置信息
         *
         * @return DataSource
         */
        @Bean(name = "enDataSource")
        @ConfigurationProperties("spring.datasource.en")
        public DataSource enDataSource() {
            return new DruidDataSource();
        }
    
        /**
         * 我们自定义的数据源DynamicRoutingDataSource添加到Spring容器里面去
         *
         * @param cnDataSource 简体中文数据库
         * @param hkDataSource 繁体中文数据库
         * @param enDataSource 英文数据库
         */
        @Bean
        @Primary
        public DynamicRoutingDataSource dataSource(DataSource cnDataSource, DataSource hkDataSource, DataSource enDataSource) {
            Map<Object, Object> targetDataSources = Maps.newHashMapWithExpectedSize(3);
            // 每个key对应一个数据源
            targetDataSources.put(DataSourceType.CNZH, cnDataSource);
            targetDataSources.put(DataSourceType.HKZH, hkDataSource);
            targetDataSources.put(DataSourceType.USEN, enDataSource);
            return new DynamicRoutingDataSource(cnDataSource, targetDataSources);
        }
    }
    
    • 配置数据源上下文以及动态数据源路由。
      首先要新建一个数据源上下文,通过 ThreadLocal 获取和设置线程安全的数据源 key,记录当前线程使用的数据源的key是什么,以及记录所有注册成功的数据源的key的集合。那么怎么通知spring用key当前的数据源呢,spring提供一个名为AbstractRoutingDataSource的抽象类,我们只需要重写determineCurrentLookupKey方法就可以,这个方法返回当前线程的数据源的key,我们只需要从我们刚刚的数据源上下文中取出我们的key即可,具体代码如下:
    /**
     * @Description: 动态数据源设置,每次访问之前设置,访问完成之后在清空
     * (AbstractRoutingDataSource相当于数据源路由中介,能有在运行时, 根据某种key值来动态切换到真正的DataSource上)
     */
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
        /**
         * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
         * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
         */
        private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
    
        public static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
    
        /**
         * 构造函数
         *
         * @param defaultTargetDataSource 默认的数据源
         * @param targetDataSources       多数据源每个key对应一个数据源
         */
        public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
            // 设置默认数据源
            super.setDefaultTargetDataSource(defaultTargetDataSource);
            // 设置多数据源. key value的形式
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }
    
        /**
         * 多数据源对应的key, 会通过这个key找到我们需要的数据源
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return getDataSource();
        }
    
        /**
         * 设置使用哪个数据源
         *
         * @param dataSource 数据源对应的名字
         */
        public static void setDataSource(DataSourceType dataSource) {
            log.info("切换到{}数据源", dataSource);
            contextHolder.set(dataSource);
        }
    
        /**
         * 获取数据源对应的名字
         *
         * @return 数据源对应的名字
         */
        public static DataSourceType getDataSource() {
            return contextHolder.get();
        }
    
        /**
         * 清空掉
         */
        public static void clearDataSource() {
            contextHolder.remove();
        }
    }
    
    
    • 数据源类型枚举类
    public enum DataSourceType {
    
        /**
         * 中文简体
         */
        CNZH,
    
        /**
         * 中文繁体
         */
        HKZH,
    
        /**
         * 美国英文
         */
        USEN
    
    }
    
    
    • 自定义注解。
      现在spring也已经知道通过key来取对应的数据源,我们需要在需要切换数据源的方法上设置数据源的key,并且保存在数据源上下文中。
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataSourceAnnotation {
    
        /**
         * 数据源类型
         * @return 数据源类型
         */
        DataSourceType sourceType();
    
    }
    
    
    • 切点可以是DataSourceAnnotation注解,所有添加了@DataSurceAnnotation的方法都进入切面,并根据传入的参数进行相应的切换。
    @Component
    @Aspect
    @Order(value = 1) //这是关键,要让该切面调用先于AbstractRoutingDataSource的determineCurrentLookupKey()
    public class DynamicDataSourceAspect {
    
        /**
         * 所有添加了DataSurceAnnotation的方法都进入切面
         */
    @Pointcut("@annotation(com.houtang.csms.mps.multisource.DataSourceAnnotation)")
        public void dataSourcePointCut() {
    
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
         
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
    
            //在执行方法之前设置使用哪个数据源
            DataSourceAnnotation ds = method.getAnnotation(DataSourceAnnotation.class);
            if (ds == null) {
                DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
            } else {
                DynamicRoutingDataSource.setDataSource(ds.sourceType());
            }
            try {
                return point.proceed();
            } finally {
                DynamicRoutingDataSource.clearDataSource();
            }
        }
    }
    
    • 可以测试一下上述方法,在测试方法上加注解,通过参数DataSourceType.CNZH切换到中文数据源。
    @DataSourceAnnotation(sourceType = DataSourceType.CNZH)
        @Test
        public void saveFaqEn() {
            FaqEn faq = new FaqEn();
            faq.setDeviceType("Lexmark CX725");
            faq.setFaqStatus(1);
            faq.setFaqSort(1);
            faq.setFaqTitle("Printer failure, unable to operate remotely");
            faq.setFaqContent("");
            faq.setFaqDate(LocalDateTime.now());
            faqEnMapper.insert(faq);
        }
    
    • 如果需求是数据库的读写分离,通过上述方法能很好的实现。但现在的需求是动态切换中英文数据库,所以我改进了一下。
      改进思路:不再通过添加注解的方式进入切面,而是在进入controller方法之前,通过请求头的Accept-Language参数动态切换数据源’。
      我们不再需要自定义注解了,主需要更改切点和切换数据源的条件。下面是更改后的:
    @Component
    @Aspect
    @Order(value = 1) //这是关键,要让该切面调用先于AbstractRoutingDataSource的determineCurrentLookupKey()
    public class DynamicDataSourceAspect {
    
        /**
         * 在所有controller接口前执行
         */
        @Pointcut("execution(* com.houtang.csms.mps.controller..*.*(..))")
        public void dataSourcePointCut() {
    
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String language = request.getHeader("Accept-Language");
            if (language == null) {
                DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
            } else if (language.equals("HK")) {
                DynamicRoutingDataSource.setDataSource(DataSourceType.HKZH);
            } else if (language.equals("EN")) {
                DynamicRoutingDataSource.setDataSource(DataSourceType.USEN);
            } else {
                DynamicRoutingDataSource.setDataSource(DataSourceType.CNZH);
            }
            try {
                return point.proceed();
            } finally {
                DynamicRoutingDataSource.clearDataSource();
            }
    }
    
    • 测试


      测试

    相关文章

      网友评论

          本文标题:SpringBoot(三)动态数据源切换

          本文链接:https://www.haomeiwen.com/subject/tbsjwctx.html