資料來源切換方法
Spring對資料來源的管理類似於策略模式,不懂策略模式也沒關係,其實就是有一個全域性的鍵值對,型別是Map<String, DataSource>
。當JDBC
運算元據庫之時,會根據不同的key值選擇不同的資料來源。而這個key值可以放到方法的註解裡。
所以切換資料來源的思路就是讓JDBC
在獲取資料來源時根據key獲取到要切換的資料來源。
JDBC
提供了AbstractRoutingDataSource
抽象類,類名意思是資料來源路由,該類提供了一個抽象方法determineCurrentLookupKey()
,切換資料來源時JDBC
會呼叫這個方法獲取資料來源的key,所以只需要實現該方法,改變該方法中返回的key值即可。
原始碼解讀
1.從類關係圖中可以看出AbstractRoutingDataSource
類實現了DataSource
介面,後者有兩個getConnection()
方法,即獲取DB連線的作用。
2.AbstractRoutingDataSource
實現了這兩個方法
其中determineTargetDataSource()
方法的作用就是獲取實際的資料來源,其內部呼叫了determineCurrentLookupKey()
方法,取到當前設定的key,透過key在上下文this.resolvedDataSources
屬性中嘗試獲取DataSource物件,這個物件即當前連線的資料來源
3.那麼this.resolvedDataSources
在哪裏維護呢? 繼續在AbstractRoutingDataSource
類裡找,可以找到afterPropertiesSet()
方法,這個方法是InitializingBean
介面的,作用是在bean的所有屬性設定完成後便會呼叫此方法。可以看到this.resolvedDataSources
是從this.targetDataSources
取的資訊。
所以只需要改變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註解來切換資料來源啦~~~