在項目中,有時候需要配置多數據源,在不同的場景下需要查詢不同的數據庫來滿足業務需求。在傳統的Spring + Mybatis 項目中,配置一個數據源如下:
<!-- 配置數據源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 註冊sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:config/mybatis-config.xml"/>
<property name="typeAliasesPackage" value="com.wch.base.domain"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!-- 配置dao接口掃描,配置sqlSessionTemplate -->
<context:component-scan base-package="com.wch.base.mapper"/>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
dataSource -> sqlSessionFactory-> sqlSessionTemplate ,三者是相呼應的,所以要配置多數據源,需要把DataSource、SqlSessionFactory、SqlSessionTemplate都配置多次,當數據源數量升上去以後,很難維護和管理,還容易出現問題。
AbstractRoutingDataSource
AbstractRoutingDataSource是spring-jdbc包提供的一個了AbstractDataSource的抽象類,它實現了DataSource接口的用於獲取數據庫連接的方法。
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
AbstractRoutingDataSource 的源碼介紹:
AbstractRoutingDataSource 的 getConnection() 方法會根據 lookup 對應不同目標的數據源,通常是(但不一定)通過某些線程綁定的事物上下文來實現。
實現原理:
- AbstractRoutingDataSource 中的 targetDataSources 用來存放需要配置個多數據源(key 可以自己定義),defaultTargetDataSource 用來存放默認的數據源。
- 在 afterPropertiesSet() 方法中會把 targetDataSources 中的數據源存放到 resolvedDataSources 中,把默認的數據源 defaultTargetDataSource 存放到 resolvedDefaultDataSource 中。
- 調用 determineCurrentLookupKey() 接口獲取當前需要使用的數據源 key,拿到對應的 DataSource 然後調用 getConnection() 返回數據庫連接。
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
/**
* Retrieve the current target DataSource. Determines the
* {@link #determineCurrentLookupKey() current lookup key}, performs
* a lookup in the {@link #setTargetDataSources targetDataSources} map,
* falls back to the specified
* {@link #setDefaultTargetDataSource default target DataSource} if necessary.
* @see #determineCurrentLookupKey()
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 獲取 key
Object lookupKey = determineCurrentLookupKey();
// 獲取當前 key 對應的數據源
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 + "]");
}
return dataSource;
}
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
整個流程很清晰,現在寫一個實現類,重寫 determineCurrentLookupKey() 接口。
/**
* 數據源動態切換路由
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceName();
}
}
DataSourceContextHolder:
/**
* 數據源上下文
*/
@Slf4j
@UtilityClass
public class DataSourceContextHolder {
/**
* 線程級別的私有變量
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public String getDataSourceName() {
return CONTEXT_HOLDER.get();
}
public void setDataSourceName(String name) {
log.info("切換到:{}數據源", name);
CONTEXT_HOLDER.set(name);
}
public void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
在查詢數據庫前,調用 DataSourceContextHolder 的 setDataSourceName() 接口,設定要查詢的數據源的 key,在 determineCurrentLookupKey() 中將存放的 key 返回即可。
本篇文章沒有實例分析,只是簡單介紹了 AbstractRoutingDataSource 的實現原理,下一篇文章配合AOP講解一下如何實現動態切換數據源。