路由键#determineCurrentLookupKey
先看一下AbstractRoutingDataSource的类图
我们可以看到,它间接实现了DataSource。是个抽象类,只有一个抽象方法#determineCurrentLookupKey()
代码语言:javascript复制java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
* 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();
......
}
通过注释与方法名我们可以知道,这个方法是来确定数据源的路由key的,那他究竟有什么用呢
核心方法#determineTargetDataSource
看代码可知,抽象方法#determineCurrentLookupKey()只有一个地方用到,即#determineTargetDataSource()
代码语言:javascript复制java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
* 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");
Object lookupKey = determineCurrentLookupKey();
//通过当前lookupKey获取数据源
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
//如果数据源为空,开启了宽松模式或者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;
}
......
}
而#determineTargetDataSource()为什么是核心方法,因为AbstractRoutingDataSource其自身就是一个DataSource,获取jdbc连接时会通过该方法获取数据源
代码语言:javascript复制java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
......
}
到这里,AbstractRoutingDataSource怎么工作的我们大概已经清楚了。但他是怎么初始化的呢?determineTargetDataSource方法中的resolvedDataSources是怎么来的呢?
初始化
代码语言:javascript复制java复制代码public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
getter/setter忽略......
@Override
public void afterPropertiesSet() {
//targetDataSources是必须的,不然bean初始化时候就会抛错
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
//已解析数据源map初始化
this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
//这一步resolveSpecifiedLookupKey其实就是返回key自身
Object lookupKey = resolveSpecifiedLookupKey(key);
//如果value是DataSource类型直接返回,是String类型则会认为是jndi名称,通过JndiDataSourceLookup类查找
DataSource dataSource = resolveSpecifiedDataSource(value);
//放入已解析数据源
this.resolvedDataSources.put(lookupKey, dataSource);
});
//设置默认数据源
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
......
}
resolveSpecifiedDataSource部分没有详细介绍,感兴趣的小伙伴们可以自行追踪源码与DataSourceLookup类的源码去查看
简单使用
上面源码看完相信大家都对AbstractRoutingDataSource很了解了,使用方面总结就三步
- 实现AbstractRoutingDataSource
- 重写#determineCurrentLookupKey()
- 设置targetDataSources与defaultTargetDataSource
新建实现类
代码语言:javascript复制java复制代码public class RoutingDataSource extends AbstractRoutingDataSource {
/**
* 获取路由key,通过key可获取已设置数据源中对应的数据源
* <p>如果lookupKey为空则获取默认数据源
* <p>详见{@link AbstractRoutingDataSource#determineTargetDataSource}
*/
@Override
protected Object determineCurrentLookupKey() {
return RoutingDataSourceContext.getRoutingKey();
}
}
新建上下文切换类
代码语言:javascript复制typescript复制代码public class RoutingDataSourceContext {
private static final ThreadLocal<String> LOOKUP_KEY_HOLDER = new ThreadLocal<>();
public static void setRoutingKey(String routingKey) {
LOOKUP_KEY_HOLDER.set(routingKey);
}
public static String getRoutingKey() {
String name = LOOKUP_KEY_HOLDER.get();
// 如果routingKey不存在则返回默认数据源
return StringUtils.hasText(name) ? name : null;
}
public static void reset() {
LOOKUP_KEY_HOLDER.remove();
}
}
注册bean
代码语言:javascript复制java复制代码@Configuration
public class DataSourceConfiguration {
/**
* routingDataSource也是dataSource
* <p>这里指定该bean为主dataSource,防止多个datasource时导致mybatis的自动装配失效
*/
@Bean
public RoutingDataSource routingDataSource() {
//数据源路由器
RoutingDataSource routingDataSource = new RoutingDataSource();
DataSource first = DataSourceBuilder.create()
.url("")
.username("")
.password("")
.driverClassName("")
.build();
DataSource second = DataSourceBuilder.create()
.url("")
.username("")
.password("")
.driverClassName("")
.build();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("first", first);
targetDataSources.put("second", second);
//设置默认数据源
routingDataSource.setDefaultTargetDataSource(first);
//设置总共支持哪些数据源切换
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
使用
代码语言:javascript复制java复制代码@Service
public class CarService {
private final CarMapper carMapper;
public CarService(CarMapper carMapper) {
this.carMapper = carMapper;
}
public Car firstGet() {
RoutingDataSourceContext.setRoutingKey("first");
return carMapper.getOne(1L);
}
}
封装成组件
很容易想到,如果能封装成组件,通过配置来动态添加数据源,并新建自定义注解通过AOP来读取就更方便了。还好我已经替你们实现啦Scindapsus-DS,而且还通过本地事务解决了AOP与Spring声明式事务冲突只能单数据源事务的问题,感兴趣的小伙伴们可以自行查看源码