路由键#determineCurrentLookupKey
先看一下AbstractRoutingDataSource的类图
我们可以看到,它间接实现了DataSource。是个抽象类,只有一个抽象方法#determineCurrentLookupKey()
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()
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连接时会通过该方法获取数据源
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是怎么来的呢?
初始化
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
新建实现类
java
复制代码
public class RoutingDataSource extends AbstractRoutingDataSource { /** * 获取路由key,通过key可获取已设置数据源中对应的数据源 * <p>如果lookupKey为空则获取默认数据源 * <p>详见{@link AbstractRoutingDataSource#determineTargetDataSource} */ @Override protected Object determineCurrentLookupKey() { return RoutingDataSourceContext.getRoutingKey(); } }
新建上下文切换类
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
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; } }
使用
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声明式事务冲突只能单数据源事务的问题,感兴趣的小伙伴们可以自行查看源码