一文弄懂spring官方多数据源

代码纪元 后端 2023-06-06

路由键#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声明式事务冲突只能单数据源事务的问题,感兴趣的小伙伴们可以自行查看源码

Apipost 私有化火热进行中

评论