SpringBoot(三)動(dòng)態(tài)數(shù)據(jù)源切換

??最近有一個(gè)項(xiàng)目國(guó)際化的需求,解決方案一般是這樣的:WEB網(wǎng)站國(guó)際化的一種解決方案。
??簡(jiǎn)單來(lái)說(shuō),國(guó)際化一方面需要配置靜態(tài)文字,另一方面需要管理動(dòng)態(tài)數(shù)據(jù)。靜態(tài)文字國(guó)際化可參考:SpringBoot項(xiàng)目國(guó)際化;;SpringBoot的國(guó)際化錯(cuò)誤信息返回,下文我們主要講的就是動(dòng)態(tài)數(shù)據(jù)國(guó)際化。
??實(shí)現(xiàn)思路:利用AOP或攔截器實(shí)現(xiàn)數(shù)據(jù)庫(kù)動(dòng)態(tài)切換。

動(dòng)態(tài)數(shù)據(jù)源切換時(shí)會(huì)遇到事務(wù)的問(wèn)題,這個(gè)問(wèn)題暫時(shí)還未考慮,下文也不涉及,這個(gè)坑留著以后再填。。(主要是太懶了)

一、準(zhǔn)備工作

  • 創(chuàng)建多個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)名分別為dev,dev_hk,dev_en,每個(gè)數(shù)據(jù)庫(kù)的表名是一樣的。
  • 添加依賴pom.xml,下面將利用AOP實(shí)現(xiàn)數(shù)據(jù)源動(dòng)態(tài)切換,所以要引入aop的依賴。
    <!-- 引入aop -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  • application.yml配置文件中配置數(shù)據(jù)源;
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
      # 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒
      minEvictableIdleTimeMillis: 30000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      # 配置監(jiān)控統(tǒng)計(jì)攔截的filters,去掉后監(jiān)控界面sql無(wú)法統(tǒng)計(jì),'wall'用于防火墻
      filters: stat,wall,log4j
      # 通過(guò)connectProperties屬性來(lái)打開(kāi)mergeSql功能;慢SQL記錄
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 自定義屬性,用于druid監(jiān)控界面的賬號(hào)、密碼配置
      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 # 邏輯已刪除值(默認(rèn)為 0)
      logic-not-delete-value: 1 # 邏輯未刪除值(默認(rèn)為 1)

二、準(zhǔn)備工作

  • 數(shù)據(jù)源配置類,獲取取application中數(shù)據(jù)源的配置,分別構(gòu)建三個(gè)數(shù)據(jù)源。
public class DynamicDataSourceConfig {

    /**
     * 簡(jiǎn)體中文數(shù)據(jù)庫(kù)   application.yml spring.datasource.cn 配置信息
     *
     * @return DataSource
     */
    @Bean(name = "cnDataSource")
    @ConfigurationProperties("spring.datasource.cn")
    public DataSource cnDataSource() {
        return new DruidDataSource();
    }

    /**
     * 繁體中文數(shù)據(jù)庫(kù)  application.yml spring.datasource.hk 配置信息
     *
     * @return DataSource
     */
    @Bean(name = "hkDataSource")
    @ConfigurationProperties("spring.datasource.hk")
    public DataSource hkDataSource() {
        return new DruidDataSource();
    }

    /**
     * 英文數(shù)據(jù)庫(kù)  application.yml  spring.datasource.en 配置信息
     *
     * @return DataSource
     */
    @Bean(name = "enDataSource")
    @ConfigurationProperties("spring.datasource.en")
    public DataSource enDataSource() {
        return new DruidDataSource();
    }

    /**
     * 我們自定義的數(shù)據(jù)源DynamicRoutingDataSource添加到Spring容器里面去
     *
     * @param cnDataSource 簡(jiǎn)體中文數(shù)據(jù)庫(kù)
     * @param hkDataSource 繁體中文數(shù)據(jù)庫(kù)
     * @param enDataSource 英文數(shù)據(jù)庫(kù)
     */
    @Bean
    @Primary
    public DynamicRoutingDataSource dataSource(DataSource cnDataSource, DataSource hkDataSource, DataSource enDataSource) {
        Map<Object, Object> targetDataSources = Maps.newHashMapWithExpectedSize(3);
        // 每個(gè)key對(duì)應(yīng)一個(gè)數(shù)據(jù)源
        targetDataSources.put(DataSourceType.CNZH, cnDataSource);
        targetDataSources.put(DataSourceType.HKZH, hkDataSource);
        targetDataSources.put(DataSourceType.USEN, enDataSource);
        return new DynamicRoutingDataSource(cnDataSource, targetDataSources);
    }
}
  • 配置數(shù)據(jù)源上下文以及動(dòng)態(tài)數(shù)據(jù)源路由。
    首先要新建一個(gè)數(shù)據(jù)源上下文,通過(guò) ThreadLocal 獲取和設(shè)置線程安全的數(shù)據(jù)源 key,記錄當(dāng)前線程使用的數(shù)據(jù)源的key是什么,以及記錄所有注冊(cè)成功的數(shù)據(jù)源的key的集合。那么怎么通知spring用key當(dāng)前的數(shù)據(jù)源呢,spring提供一個(gè)名為AbstractRoutingDataSource的抽象類,我們只需要重寫(xiě)determineCurrentLookupKey方法就可以,這個(gè)方法返回當(dāng)前線程的數(shù)據(jù)源的key,我們只需要從我們剛剛的數(shù)據(jù)源上下文中取出我們的key即可,具體代碼如下:
/**
 * @Description: 動(dòng)態(tài)數(shù)據(jù)源設(shè)置,每次訪問(wèn)之前設(shè)置,訪問(wèn)完成之后在清空
 * (AbstractRoutingDataSource相當(dāng)于數(shù)據(jù)源路由中介,能有在運(yùn)行時(shí), 根據(jù)某種key值來(lái)動(dòng)態(tài)切換到真正的DataSource上)
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    /**
     * 使用ThreadLocal維護(hù)變量,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,
     * 所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。
     */
    private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();

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

    /**
     * 構(gòu)造函數(shù)
     *
     * @param defaultTargetDataSource 默認(rèn)的數(shù)據(jù)源
     * @param targetDataSources       多數(shù)據(jù)源每個(gè)key對(duì)應(yīng)一個(gè)數(shù)據(jù)源
     */
    public DynamicRoutingDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        // 設(shè)置默認(rèn)數(shù)據(jù)源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        // 設(shè)置多數(shù)據(jù)源. key value的形式
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /**
     * 多數(shù)據(jù)源對(duì)應(yīng)的key, 會(huì)通過(guò)這個(gè)key找到我們需要的數(shù)據(jù)源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    /**
     * 設(shè)置使用哪個(gè)數(shù)據(jù)源
     *
     * @param dataSource 數(shù)據(jù)源對(duì)應(yīng)的名字
     */
    public static void setDataSource(DataSourceType dataSource) {
        log.info("切換到{}數(shù)據(jù)源", dataSource);
        contextHolder.set(dataSource);
    }

    /**
     * 獲取數(shù)據(jù)源對(duì)應(yīng)的名字
     *
     * @return 數(shù)據(jù)源對(duì)應(yīng)的名字
     */
    public static DataSourceType getDataSource() {
        return contextHolder.get();
    }

    /**
     * 清空掉
     */
    public static void clearDataSource() {
        contextHolder.remove();
    }
}

  • 數(shù)據(jù)源類型枚舉類
public enum DataSourceType {

    /**
     * 中文簡(jiǎn)體
     */
    CNZH,

    /**
     * 中文繁體
     */
    HKZH,

    /**
     * 美國(guó)英文
     */
    USEN

}

  • 自定義注解。
    現(xiàn)在spring也已經(jīng)知道通過(guò)key來(lái)取對(duì)應(yīng)的數(shù)據(jù)源,我們需要在需要切換數(shù)據(jù)源的方法上設(shè)置數(shù)據(jù)源的key,并且保存在數(shù)據(jù)源上下文中。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceAnnotation {

    /**
     * 數(shù)據(jù)源類型
     * @return 數(shù)據(jù)源類型
     */
    DataSourceType sourceType();

}

  • 切點(diǎn)可以是DataSourceAnnotation注解,所有添加了@DataSurceAnnotation的方法都進(jìn)入切面,并根據(jù)傳入的參數(shù)進(jìn)行相應(yīng)的切換。
@Component
@Aspect
@Order(value = 1) //這是關(guān)鍵,要讓該切面調(diào)用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {

    /**
     * 所有添加了DataSurceAnnotation的方法都進(jìn)入切面
     */
@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();

        //在執(zhí)行方法之前設(shè)置使用哪個(gè)數(shù)據(jù)源
        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();
        }
    }
}
  • 可以測(cè)試一下上述方法,在測(cè)試方法上加注解,通過(guò)參數(shù)DataSourceType.CNZH切換到中文數(shù)據(jù)源。
@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);
    }
  • 如果需求是數(shù)據(jù)庫(kù)的讀寫(xiě)分離,通過(guò)上述方法能很好的實(shí)現(xiàn)。但現(xiàn)在的需求是動(dòng)態(tài)切換中英文數(shù)據(jù)庫(kù),所以我改進(jìn)了一下。
    改進(jìn)思路:不再通過(guò)添加注解的方式進(jìn)入切面,而是在進(jìn)入controller方法之前,通過(guò)請(qǐng)求頭的Accept-Language參數(shù)動(dòng)態(tài)切換數(shù)據(jù)源’。
    我們不再需要自定義注解了,主需要更改切點(diǎn)和切換數(shù)據(jù)源的條件。下面是更改后的:
@Component
@Aspect
@Order(value = 1) //這是關(guān)鍵,要讓該切面調(diào)用先于AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {

    /**
     * 在所有controller接口前執(zhí)行
     */
    @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();
        }
}
  • 測(cè)試


    測(cè)試
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容