在上一篇博客《Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘》介绍了dynamic-datasource-spring-boot-starter的自动配置类和配置属性类之后,本文继续来剖析多数据源是如何切换的,什么时候切换的。
前文中提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候,会创建一个Advisor bean。
@Role(2)@Beanpublic Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(this.properties.isAllowedPublicOnly(), dsProcessor);DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);advisor.setOrder(this.properties.getOrder());return advisor;}
该bean负责处理带有@DS注解的类或方法。它发挥的作用和通过@Pointcut+@Before或@Around发挥的作用一致。更多细节可见《探索微服务中的权限控制:一次线上问题排查的思考》
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {private final Advice advice;private final Pointcut pointcut;public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {// 初始化的时候,传入了dynamicDataSourceAnnotationInterceptor// 说明拦截器发挥了通知的作用。this.advice = dynamicDataSourceAnnotationInterceptor;this.pointcut = buildPointcut();}// 核心方法, 切点获取,类比@Pointcut注解 @Overridepublic Pointcut getPointcut() {return this.pointcut;}// 核心方法,通知获取, 类比@Before, @After, @Around注解@Overridepublic Advice getAdvice() {return this.advice;}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {if (this.advice instanceof BeanFactoryAware) {((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);}}private Pointcut buildPointcut() {Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);Pointcut mpc = new AnnotationMethodPoint(DS.class);return new ComposablePointcut(cpc).union(mpc);}……
}
DynamicDataSourceAnnotationAdvisor类在初始化的时候,传入了DynamicDataSourceAnnotationInterceptor类型的拦截器作为advice, 说明该拦截器发挥了通知的作用(类似@Before, @Around)。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {/*** The identification of SPEL.*/private static final String DYNAMIC_PREFIX = "#";private final DataSourceClassResolver dataSourceClassResolver;private final DsProcessor dsProcessor;public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);this.dsProcessor = dsProcessor;}// 核心方法, dsKey就是@DS注解中的值, 如@DS("cusOB"),那么dsKey = "cusOB"@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {String dsKey = determineDatasourceKey(invocation);DynamicDataSourceContextHolder.push(dsKey);try {return invocation.proceed();} finally {DynamicDataSourceContextHolder.poll();}}private String determineDatasourceKey(MethodInvocation invocation) {String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;}
}
在invoke方法执行前,会先把该数据源对应的key加入到线程本地变量的双端队列中,如下。
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {@Overrideprotected Deque<String> initialValue() {return new ArrayDeque<>();}};
在invoke方法执行后,会先把该数据源对应的key从线程本地变量的双端队列中移除。
看到这里,我们知道,dynamic-datasource-spring-boot-starter会通过DynamicDataSourceAnnotationInterceptor 拦截器获取到@DS注解标识的方法或者类的代理。而@DS注解标注的就是数据源的Key。有了该key, Mybatis就可以获取到该key对应的Druid数据库连接池了。具体的策略,dynamic-datasource-spring-boot-starter提供了轮询和随机两种,这里就不再赘述了。
数据源的切换逻辑在DynamicRoutingDataSource类中。它继承了AbstractRoutingDataSource类,该类实现了getConnection()方法。
public abstract class AbstractRoutingDataSource extends AbstractDataSource {protected abstract DataSource determineDataSource();@Overridepublic Connection getConnection() throws SQLException {String xid = TransactionContext.getXID();if (StringUtils.isEmpty(xid)) {// 核心方法determineDataSourcereturn determineDataSource().getConnection();} else {String ds = DynamicDataSourceContextHolder.peek();ConnectionProxy connection = ConnectionFactory.getConnection(ds);return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;}}@Overridepublic Connection getConnection(String username, String password) throws SQLException {String xid = TransactionContext.getXID();if (StringUtils.isEmpty(xid)) {return determineDataSource().getConnection(username, password);} else {String ds = DynamicDataSourceContextHolder.peek();ConnectionProxy connection = ConnectionFactory.getConnection(ds);return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password)): connection;}}
}
DynamicDataSourceAnnotationInterceptor的invoke方法负责放数据源的key,
determineDataSource方法负责从线程本地变量的双端队列中取出数据源的key。
public DataSource determineDataSource() {String dsKey = DynamicDataSourceContextHolder.peek();return getDataSource(dsKey);}
getDataSource方法用来获取具体的数据库连接。
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();public DataSource getDataSource(String ds) {if (StringUtils.isEmpty(ds)) {return determinePrimaryDataSource();} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {log.debug("dynamic-datasource switch to the datasource named [{}]", ds);return groupDataSources.get(ds).determineDataSource();} else if (dataSourceMap.containsKey(ds)) {log.debug("dynamic-datasource switch to the datasource named [{}]", ds);// map中存了所有的数据库连接信息return dataSourceMap.get(ds);}if (strict) {throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);}return determinePrimaryDataSource();}
那么问题来了,dataSourceMap的数据库相关信息是什么时候存进去的呢?
大家还记得上一篇博客中,我们提到dynamic-datasource-spring-boot-starter的自动配置类DynamicDataSourceAutoConfiguration在初始化的时候还创建了一个DynamicDataSourceProvider类型的bean嘛?没错,dataSourceMap是它从DynamicDataSourceProperties配置类中获取,然后存储进去的,key是数据源的唯一标识,如value是数据库的相关信息,包括url, username, password等。
@Bean@ConditionalOnMissingBean // 确保Spring容器中不存在 DynamicDataSourceProvider 类型的 Bean 时才创建该 Beanpublic DynamicDataSourceProvider dynamicDataSourceProvider() {Map<String, DataSourceProperty> datasourceMap = this.properties.getDatasource();return new YmlDynamicDataSourceProvider(datasourceMap);}public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {/*** 所有数据源*/private final Map<String, DataSourceProperty> dataSourcePropertiesMap;@Overridepublic Map<String, DataSource> loadDataSources() {// 核心方法return createDataSourceMap(dataSourcePropertiesMap);}
}
protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {// dataSourcePropertiesMap 就是从DynamicDataSourceProperties配置类中拿到的所有数据库信息Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {DataSourceProperty dataSourceProperty = item.getValue();String poolName = dataSourceProperty.getPoolName();if (poolName == null || "".equals(poolName)) {poolName = item.getKey();}dataSourceProperty.setPoolName(poolName);dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));}return dataSourceMap;}
最后,我们来总结下,dynamic-datasource-spring-boot-starter本质还是通过AOP实现了多数据源的切换,实现思路和编码规范、风格等都值得大家深入研究和学习。先研究透一个,其它的SpringBoot的常用starter就会手到擒来。