SpringBoot(三)动态数据源切换

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

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

一、准备工作

  • 创建多个数据库,数据库名分别为dev,dev_hk,dev_en,每个数据库的表名是一样的。
  • 添加依赖pom.xml,下面将利用AOP实现数据源动态切换,所以要引入aop的依赖。
    
    
        org.springframework.boot
        spring-boot-starter-aop
    
  • 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 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 contextHolder = new ThreadLocal<>();

    public static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);

    /**
     * 构造函数
     *
     * @param defaultTargetDataSource 默认的数据源
     * @param targetDataSources       多数据源每个key对应一个数据源
     */
    public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map 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(三)动态数据源切换)