在项目中,有时候需要配置多数据源,在不同的场景下需要查询不同的数据库来满足业务需求。在传统的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讲解一下如何实现动态切换数据源。