切換語言為:簡體
SpringBoot配置動態資料來源詳細步驟

SpringBoot配置動態資料來源詳細步驟

  • 爱糖宝
  • 2024-08-29
  • 2051
  • 0
  • 0

資料來源切換方法

Spring對資料來源的管理類似於策略模式,不懂策略模式也沒關係,其實就是有一個全域性的鍵值對,型別是Map<String, DataSource>。當JDBC運算元據庫之時,會根據不同的key值選擇不同的資料來源。而這個key值可以放到方法的註解裡。

所以切換資料來源的思路就是讓JDBC在獲取資料來源時根據key獲取到要切換的資料來源。

JDBC提供了AbstractRoutingDataSource抽象類,類名意思是資料來源路由,該類提供了一個抽象方法determineCurrentLookupKey(),切換資料來源時JDBC會呼叫這個方法獲取資料來源的key,所以只需要實現該方法,改變該方法中返回的key值即可。

原始碼解讀

1.從類關係圖中可以看出AbstractRoutingDataSource類實現了DataSource介面,後者有兩個getConnection()方法,即獲取DB連線的作用。

SpringBoot配置動態資料來源詳細步驟

2.AbstractRoutingDataSource實現了這兩個方法

SpringBoot配置動態資料來源詳細步驟

其中determineTargetDataSource()方法的作用就是獲取實際的資料來源,其內部呼叫了determineCurrentLookupKey()方法,取到當前設定的key,透過key在上下文this.resolvedDataSources屬性中嘗試獲取DataSource物件,這個物件即當前連線的資料來源

SpringBoot配置動態資料來源詳細步驟

3.那麼this.resolvedDataSources在哪裏維護呢? 繼續在AbstractRoutingDataSource類裡找,可以找到afterPropertiesSet()方法,這個方法是InitializingBean介面的,作用是在bean的所有屬性設定完成後便會呼叫此方法。可以看到this.resolvedDataSources是從this.targetDataSources取的資訊。

SpringBoot配置動態資料來源詳細步驟

所以只需要改變this.targetDataSources,即可改變this.resolvedDataSources;後續改變determineCurrentLookupKey()的返回值(key),在呼叫getConnection()時即可獲取到指定的資料來源


實現方式:註解+切面

別看步驟挺多,但其實很容易理解和使用

1.配置檔案示例:

spring:
  datasource: 
    master: # 資料來源1
      jdbc-url: jdbc:mysql://localhost:3306/master?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    db1: # 資料來源1
      jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

2.建立資料來源配置類

建立資料來源配置類(我這裏爲了方便區分就為每一個數據源建立了一個配置類,當然也可以把所有的資料來源配置在一個類裡)

@Configuration
@EnableConfigurationProperties({MasterDataSourceProperties.class})
public class MasterDataSourceConfig {
    /**
    * 這個MasterDataSourceProperties是讀取配置檔案的類,我這裏爲了省篇幅就不展示了
    **/
    @Autowired
    private MasterDataSourceProperties masterDataSourceProperties;

    @Bean
    @Primary
    public DataSource masterDataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(masterDataSourceProperties.getUrl());
        datasource.setUsername(masterDataSourceProperties.getUsername());
        datasource.setPassword(AESUtil.aesDecode(masterDataSourceProperties.getPassword()));
        datasource.setDriverClassName(masterDataSourceProperties.getDriverClassName());
        ......

        return datasource;
    }
}

@Configuration
@EnableConfigurationProperties({OdsDataSourceProperties.class})
public class DB1DataSourceConfig {
    /**
    * 這個DB1DataSourceProperties是讀取配置檔案的類,我這裏爲了省篇幅就不展示了
    **/
    @Autowired
    private DB1DataSourceProperties dB1DataSourceProperties;

    @Bean
    public DataSource db1DataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(dB1DataSourceProperties.getUrl());
        datasource.setUsername(dB1DataSourceProperties.getUsername());
        datasource.setPassword(AESUtil.aesDecode(dB1DataSourceProperties.getPassword()));
        datasource.setDriverClassName(dB1DataSourceProperties.getDriverClassName());
        ......

        return datasource;
    }
}

3.建立DynamicDataSource

建立自己的一個DynamicDataSource類(名字任意)繼承AbstractRoutingDataSource,維護資料來源,提供切換方法。

public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
    * 如果不希望資料來源在啟動配置時就載入好,可以定製這個方法,從任何你希望的地方讀取並返回資料來源
    * 比如從資料庫、檔案、外部介面等讀取資料來源資訊,並最終返回一個DataSource實現類物件即可
    */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
    * 如果希望所有資料來源在啟動配置時就載入好,然後透過設定資料來源Key值來切換資料,定製這個方法
    */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
    * 設定預設資料來源
    *
    * @param defaultDataSource
    */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
    * 設定資料來源
    *
    * @param dataSources
    */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 將資料來源的 key 放到資料來源上下文的 key 集合中,用於切換時判斷資料來源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}

4.建立資料來源上下文處理器DynamicDataSourceContextHolder

建立資料來源上下文處理器DynamicDataSourceContextHolder用以儲存當前執行緒需要使用的資料來源名稱。

public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 將 master 資料來源的 key作為預設資料來源的 key
         */
        @Override
        protected String initialValue() {
            return "master";
        }
    };


    /**
     * 資料來源的 key集合,用於切換時判斷資料來源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 切換資料來源
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 獲取資料來源
     *
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置資料來源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判斷是否包含資料來源
     *
     * @param key 資料來源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 新增資料來源keys
     *
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

5.建立資料來源配置類DataSourceConfig

建立資料來源配置類DataSourceConfig,將所有資料來源注入到spring容器

@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自動配置,避免環形呼叫
public class DataSourceConfig {

    @Autowired
    private MasterDataSourceConfig masterDataSourceConfig;

    @Autowired
    private DB1DataSourceConfig dB1DataSourceConfig;
    /**
     * 設定動態資料來源為主資料來源
     *
     * @return
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 預設指定的資料來源
        dynamicDataSource.setDefaultDataSource(masterDataSourceConfig.masterDataSource());
        // 將資料來源設定進map
        Map<Object, Object> dataSourceMap = new HashMap<>(8);
        dataSourceMap.put(DataSourceEnum.MASTER.toString(), masterDataSourceConfig.masterDataSource());
        dataSourceMap.put(DataSourceEnum.DB1.toString(), dB1DataSourceConfig.db1DataSource());
        // 使用 Map 儲存多個數據源,並設定到動態資料來源物件中,這個值最終會在afterPropertiesSet中被設定到resolvedDataSources上
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }
}

6.建立資料來源型別列舉DataSourceEnum

public enum DataSourceEnum {

    /**預設型別*/
    MASTER,
    /**DB1型別*/
    DB1,
    ;
}

7.建立自定義註解@DataSource

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    /**
     * 資料來源key值
     * @return
     */
    DataSourceEnum value();
}

8.建立切面DynamicDataSourceAspect

@Slf4j
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {

    /**
     * 切換資料來源
     *
     * @param point
     * @param dataSource
     */
    @Before("@annotation(dataSource))")
    public void switchDataSource(JoinPoint point, DataSource dataSource) {
        if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().toString())) {
            log.info("DataSource [{}] doesn't exist, use default DataSource", dataSource.value());
        } else {
            // 切換資料來源
            DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().toString());
            log.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(),
                    point.getSignature());
        }
    }

    /**
     * 重置資料來源
     *
     * @param point
     * @param dataSource
     */
    @After("@annotation(dataSource))")
    public void restoreDataSource(JoinPoint point, DataSource dataSource) {
        // 將資料來源置為預設資料來源
        DynamicDataSourceContextHolder.clearDataSourceKey();
        log.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
    }
}

如何使用

@Override
@DataSource(DataSourceEnum.DB1)
public Page<AuditTaskDto> queryAuditTask(AuditTaskQuery query) {
    Page<AuditTaskDto> page = baseMapper.queryAuditTask(query);
    return page;
}

如此就可以直接使用自定義的@DataSource註解來切換資料來源啦~~~

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.