AbstractRoutingDataSource 實現動態資料來源切換原理簡單分析
寫在前面,專案中用到了動態資料來源切換,記錄一下其執行機制。
程式碼展示
下面列出一些關鍵程式碼,後續分析會用到
- 資料配置
@Configuration
@PropertySource({ "classpath:jdbc.yml" })
@EnableTransactionManagement(proxyTargetClass = true)
public class DataConfig {
@Autowired
private Environment env ;
/**
* 將jdbc相關的異常轉換為spring的異常型別
*/
@Bean
public BeanPostProcessor persistenceTransLation(){
return new PersistenceExceptionTranslationPostProcessor() ;
}
/**
* 多資料來源
* @return
*/
@Bean
public DynamicDataSource dynamicDataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object,Object> sourceMap = new HashMap<>();
//取得所有的datasource,DataSourceEnum裡存放資料來源的唯一標識
EnumSet<DataSourceEnum> enums = EnumSet.allOf(DataSourceEnum.class);
for(DataSourceEnum dataSource:enums){
// map存放資料來源的key和資料來源
sourceMap.put(dataSource.getKey(),generateDataSource(dataSource.getKey()));
}
// ? 重點
dynamicDataSource.setTargetDataSources(sourceMap);
dynamicDataSource.setDefaultTargetDataSource(sourceMap.get(DataSourceEnum.TEST.getKey()));
return dynamicDataSource;
}
// 讀取配置檔案,建立資料來源物件
private EncryptDataSource generateDataSource(String key){
EncryptDataSource dataSource
= new EncryptDataSource();
key = key.toLowerCase() ;
String url = "jdbc.url."+key;
String username = "jdbc.username."+key;
String password = "jdbc.password."+key;
dataSource.setDriverClassName("com.sybase.jdbc4.jdbc.SybDataSource");//SybDriver
dataSource.setUrl(env.getProperty(url));
dataSource.setUsername(env.getProperty(username));
dataSource.setPassword(env.getProperty(password));
//配置連線池
dataSource.setInitialSize(Integer.parseInt(env.getProperty("jdbc.initialSize")));
dataSource.setMaxIdle(Integer.parseInt(env.getProperty("jdbc.maxIdle")));
dataSource.setMinIdle(Integer.parseInt(env.getProperty("jdbc.minIdle")));
return dataSource;
}
}
- 自定義資料來源類
public class DynamicDataSource extends AbstractRoutingDataSource {
// 存放資料來源的id(唯一標識)
private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>() ;
// ? 重點
@Override
protected Object determineCurrentLookupKey() {
return dataSourceHolder.get();
}
// 切換資料來源
public static void router(String sourceKey){
if(StrUtil.isEmpty(sourceKey)){
return;
}
if(DataSourceEnum.getSourceByKey(sourceKey)!=null){
//根據法院程式碼切換
dataSourceHolder.set(DataSourceEnum.getSourceByKey(sourceKey));
}
}
……
}
- 資料來源配置(jdbc.yml)
#測試庫
jdbc.url.test: jdbc:sybase:Tds:xxx.xxx.xxx.xxx:xx/JUDGE?charset=cp936
jdbc.username.test: fymis
jdbc.password.test: xx
原理分析
第一部分已將關鍵程式碼列出,該部分通過修改後即可實現資料來源的切換功能。下面來分析一下流程。
AbstractRoutingDataSource 類解析
只列出了部分方法,需要詳細程式碼請自行移步原始碼
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;// 目標資料來源map
@Nullable
private Object defaultTargetDataSource;// 預設資料來源
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
public AbstractRoutingDataSource() {
}
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
// 初始化 Bean 時執行
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
// 將targetDataSources屬性的值賦值給resolvedDataSources,後續需要用到resolvedDataSources
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
// 存放資料來源唯一標識和資料來源物件
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
// 重寫了 getConnection 方法,ORM 框架執行語句前會呼叫該處
@Override
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
// 同上
@Override
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}
// ? 重點
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 呼叫我們重寫的determineCurrentLookupKey方法,返回的是資料來源的唯一標識
Object lookupKey = this.determineCurrentLookupKey();
// 從map中查詢該標識對應的資料來源,然後方法返回該資料來源,呼叫 getConnection 開啟對應連線
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
// 鉤子方法,供我們重寫
@Nullable
protected abstract Object determineCurrentLookupKey();
}
總結與閒談
綜上,可以列出以下幾點描述整個流程:
- 自定義類繼承
AbstractRoutingDataSource
(後文稱 ARDS),重寫determineCurrentLookupKey()
,返回資料來源的唯一標識; - 將資料來源名稱和資料來源封裝為 map,呼叫 ARDS 類的
setTargetDataSources()
設定目標資料來源。ARDS
類實現了InitializingBean
介面,重寫了afterPropertySet()
(對該方法不熟悉的話請回顧一下 Bean 的生命週期,該方法在 Bean 的屬性注入後執行),該方法內部對 resolvedDataSources 屬性賦值(將targetDataSources
的值放進去),後續會用到 resolvedDataSources ; - ARDS 實現了 DataSource 介面,重寫了
getConnection()
,當 ORM 框架執行 sql 語句前總是執行 getConnection(),然後就呼叫到了重寫後的 getConnection(),該方法內部呼叫了 ARDS 類的determineTargetDataSource()
; - determineTargetDataSource() 內部呼叫了自定義類重寫的
determineCurrentLookupKey()
,返回資料來源的對映,然後從 resolvedDataSources(map) 屬性獲取到資料來源,進行後續的操作。
(題外話)想要實現資料來源切換可以有兩種實現:
- 手動切換資料來源,每次執行相應操作前呼叫
router
方法切換; - 還有一種思路就是利用 AOP,設計一個註解,註解內新增資料來源唯一標識的屬性,然後對方法新增註解,AOP 程式碼進行攔截,然後將唯一標識賦值給
ThreadLocal
變數即可。