目录
一、简介
dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
github: https://github.com/baomidou/dynamic-datasource-spring-boot-starter
文档: https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki
它与mybatis-plus属于同一个生态圈,很容易集成mybatis-plus。
特性:
1. 数据源分组,适用于多种场景:纯粹多库,读写分离,一主多从,混合模式。
2. 内置敏感参数加密和启动初始化表结构schema数据库database。
3. 提供对Druid、Mybatis-Plus、P6sy和Jndi的快速集成。
4. 简化Druid和HikariCp配置,提供全局参数配置。
5. 提供自定义数据源来源接口(默认使用yml或properties配置)。
6. 提供项目启动后增减数据源方案。
7. 提供Mybatis环境下的纯读写分离方案。
8. 使用spel动态参数解析数据源,如从session、header或参数中获取数据源。(多租户架构神器)
9. 提供多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC,每个Service都是不同的数据源)
10. 提供“不使用注解而使用正则或 spel”来切换数据源方案(实验性功能)。
11. 基于seata的分布式事务支持。
无论是动态增减数据源、数据源分组,还是纯粹多库、读写分离、一主多从、从其他数据库或者配置中心读取数据源,比起Mapper分包方式或自定义AOP注解切片方式实现多数据源方案,使用dynamic-datasource-spring-boot-starter要便捷许多,极大简化了工作量。
具体使用,请参考示例:
springboot整合mybatis-plus、druid连接池和多数据源配置_WorldMvp的专栏-CSDN博客_druid连接池多数据源
二、源码分析
本文源码解析基于3.3.1版本。由于篇幅限制,只截了重点代码,如果需要看完整代码,可以去github拉取。
2.1 整体结构
整体结构如下图所示:
拿到代码后,要找到入手点,带着问题阅读代码。
2.2 自动配置怎么实现的
一般情况下,一个starter的最好入手点就是自动配置类,在 META-INF/spring.factories文件中指定自动配置类入口。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
在spring.factories中,可以看到这个工程的自动配置类路径。从核心自动配置类DynamicDataSourceAutoConfiguration入手,可以认为这就是程序的Main入口。
/**
* 动态数据源核心自动配置类
*/
@Slf4j
@Configuration
@AllArgsConstructor
// 读取以spring.datasource.dynamic为前缀的配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
// 需要在spring boot的DataSource bean自动配置之前注入我们的DataSource bean
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
// 引入了Druid的autoConfig和各种数据源连接池的Creator
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
// 当含有spring.datasource.dynamic配置的时候,启用这个autoConfig
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
private final DynamicDataSourceProperties properties;
/**
* 多数据源加载接口,默认从yml中读取多数据源配置
* @return DynamicDataSourceProvider
*/
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
/**
* 注册自己的动态多数据源DataSource
* @param dynamicDataSourceProvider 各种数据源连接池创建者
* @return DataSource
*/
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
/**
* AOP切面,对DS注解过的方法进行增强,达到切换数据源的目的。
* @param dsProcessor 动态参数解析数据源。如果数据源名称以#开头,就会进入这个解析器链。
* @return advisor
*/
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
// aop方法拦截器在方法调用前后做操作,设置动态参数解析器
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
// 使用AbstractPointcutAdvisor将pointcut和advice连接构成切面
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}
/**
* seata分布式事务支持
*
*/
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
@Bean
public Advisor dynamicTransactionAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
return new DefaultPointcutAdvisor(pointcut, new DynamicTransactionAdvisor());
}
/**
* 动态参数解析器链
* @return DsProcessor
*/
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor() {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
}
这里自动配置的五个Bean都是非常重要的。
自动配置类的几个注解都写了注释,其中重要的是这个注解:
// 读取以spring.datasource.dynamic为前缀的配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@EnableConfigurationProperties:使 @ConfigurationProperties 注解的类生效,主要是用来把properties或者yml配置文件转化为bean来使用,这个在实际使用中非常实用。
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {
public static final String PREFIX = "spring.datasource.dynamic";
public static final String HEALTH = PREFIX + ".health";
/**
* 必须设置默认的库,默认master
*/
private String primary = "master";
/**
* 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
*/
private Boolean strict = false;
/**
* 是否使用p6spy输出,默认不输出
*/
private Boolean p6spy = false;
/**
* 是否使用开启seata,默认不开启
*/
private Boolean seata = false;
/**
* seata使用模式,默认AT
*/
private SeataMode seataMode = SeataMode.AT;
/**
* 是否使用 spring actuator 监控检查,默认不检查
*/
private boolean health = false;
/**
* 每一个数据源
*/
private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();
/**
* 多数据源选择算法clazz,默认负载均衡算法
*/
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
/**
* aop切面顺序,默认优先级最高
*/
private Integer order = Ordered.HIGHEST_PRECEDENCE;
/**
* Druid全局参数配置
*/
@NestedConfigurationProperty
private DruidConfig druid = new DruidConfig();
/**
* HikariCp全局参数配置
*/
@NestedConfigurationProperty
private HikariCpConfig hikari = new HikariCpConfig();
/**
* 全局默认publicKey
*/
private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING;
/**
* aop 切面是否只允许切 public 方法
*/
private boolean allowedPublicOnly = true;
}
可以发现,我们在spring.datasource.dynamic配置的属性都会注入到这个配置Bean中。需要注意的是,使用了@NestedConfigurationProperty嵌套了其他的配置类。如果不清楚配置项是什么,看看DynamicDataSourceProperties这个类就清楚了。比如DruidConfig,这个DruidConfig是自定义的一个配置类,不是Druid里面的,它下面有个toProperties方法,为了实现yml配置中每个dataSource下面的durid可以独立配置(若不独立配置,则使用全局配置),根据全局配置和独立配置结合转换为Properties,然后在DruidDataSourceCreator类中根据这个配置创建druid连接池。
2.3 如何集成众多连接池
集成连接池配置项是通过DynamicDataSourceProperties配置类实现的,但是如何通过这些配置项生成真正的数据源连接池?让我们来看creator包。
见名知意,可知支持哪些类型的数据源。
在自动装配中配置DataSource的时候,new了一个DynamicRoutingDataSource,该类实现了InitializingBean接口,在bean初始化时做一些操作。
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final String UNDERLINE = "_";
/**
* 所有数据库
*/
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
/**
* 分组数据库
*/
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
@Setter
private DynamicDataSourceProvider provider;
@Setter
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
@Setter
private String primary = "master";
@Setter
private Boolean strict = false;
@Setter
private Boolean p6spy = false;
@Setter
private Boolean seata = false;
@Override
public DataSource determineDataSource() {
return getDataSource(DynamicDataSourceContextHolder.peek());
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
/**
* 获取当前所有的数据源
*
* @return 当前所有数据源
*/
public Map<String, DataSource> getCurrentDataSources() {
return dataSourceMap;
}
/**
* 获取的当前所有的分组数据源
*
* @return 当前所有的分组数据源
*/
public Map<String, GroupDataSource> getCurrentGroupDataSources() {
return groupDataSources;
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
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);
return dataSourceMap.get(ds);
}
if (strict) {
throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
/**
* 添加数据源
*
* @param ds 数据源名称
* @param dataSource 数据源
*/
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
// 新数据源添加到分组
this.addGroupDataSource(ds, dataSource);
// 关闭老的数据源
if (oldDataSource != null) {
try {
closeDataSource(oldDataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
}
log.info("dynamic-datasource - load a datasource named [{}] success", ds);
}
/**
* 新数据源添加到分组
*
* @param ds 新数据源的名字
* @param dataSource 新数据源
*/
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
GroupDataSource groupDataSource = groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
groupDataSources.put(group, groupDataSource);
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
/**
* 删除数据源
*
* @param ds 数据源名称
*/
public synchronized void removeDataSource(String ds) {
if (!StringUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
}
if (primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
}
if (dataSourceMap.containsKey(ds)) {
DataSource dataSource = dataSourceMap.remove(ds);
try {
closeDataSource(dataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
if (oldDataSource == null) {
if (log.isWarnEnabled()) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
}
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
/**
* 关闭数据源。
* <pre>
* 从3.2.0开启,如果是原生或使用 DataSourceCreator 创建的数据源会包装成ItemDataSource。
* ItemDataSource保留了最原始的数据源,其可直接关闭。
* 如果不是DataSourceCreator创建的数据源则只有尝试解包装再关闭。
* </pre>
*/
private void closeDataSource(DataSource dataSource) throws Exception {
if (dataSource instanceof ItemDataSource) {
((ItemDataSource) dataSource).close();
} else {
if (seata && dataSource instanceof DataSourceProxy) {
DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
dataSource = dataSourceProxy.getTargetDataSource();
}
if (p6spy && dataSource instanceof P6DataSource) {
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
realDataSourceField.setAccessible(true);
dataSource = (DataSource) realDataSourceField.get(dataSource);
}
Class<? extends DataSource> clazz = dataSource.getClass();
Method closeMethod = clazz.getDeclaredMethod("close");
closeMethod.invoke(dataSource);
}
}
@Override
public void destroy() throws Exception {
log.info("dynamic-datasource start closing ....");
for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
closeDataSource(item.getValue());
}
log.info("dynamic-datasource all closed success,bye");
}
@Override
public void afterPropertiesSet() throws Exception {
// 检查开启了配置但没有相关依赖
checkEnv();
// 添加并分组数据源
Map<String, DataSource> dataSources = provider.loadDataSources();
for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
addDataSource(dsItem.getKey(), dsItem.getValue());
}
// 检测默认数据源是否设置
if (groupDataSources.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
} else if (dataSourceMap.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
} else {
throw new RuntimeException("dynamic-datasource Please check the setting of primary");
}
}
private void checkEnv() {
if (p6spy) {
try {
Class.forName("com.p6spy.engine.spy.P6DataSource");
log.info("dynamic-datasource detect P6SPY plugin and enabled it");
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
}
}
if (seata) {
try {
Class.forName("io.seata.rm.datasource.DataSourceProxy");
log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
}
}
}
}
这个类就是核心动态数据源组件。它将DataSource维护在map里,这里重点看如何创建数据源连接池。它所做的操作就是:初始化时从provider获取创建好的数据源map,然后解析这个map对其分组。下面来看看这个provider里面是如何创建这个数据源map的。
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
在自动装配中注入的这个bean,是通过yml读取配置文件(还有通过jdbc读取配置文件)生成的。
通过跟踪provider.loadDataSources()方法,发现在createDataSourceMap()方法中调用的是dataSourceCreator.createDataSource(dataSourceProperty, publicKey)。进一步追踪可以发现,具体使用哪种类型的连接池,是在DynamicDataSourceCreatorAutoConfiguration自动配置类中完成的。
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {
private static final int JNDI_ORDER = 1000;
private static final int DRUID_ORDER = 2000;
private static final int HIKARI_ORDER = 3000;
private static final int DEFAULT_ORDER = 5000;
private final DynamicDataSourceProperties properties;
@Primary
@Bean
@ConditionalOnMissingBean
public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {
DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();
defaultDataSourceCreator.setProperties(properties);
defaultDataSourceCreator.setDataSourceCreators(dataSourceCreators);
return defaultDataSourceCreator;
}
@Bean
@Order(DEFAULT_ORDER)
@ConditionalOnMissingBean
public BasicDataSourceCreator basicDataSourceCreator() {
return new BasicDataSourceCreator();
}
@Bean
@Order(JNDI_ORDER)
@ConditionalOnMissingBean
public JndiDataSourceCreator jndiDataSourceCreator() {
return new JndiDataSourceCreator();
}
/**
* 存在Druid数据源时, 加入创建器
*/
@ConditionalOnClass(DruidDataSource.class)
@Configuration
public class DruidDataSourceCreatorConfiguration {
@Bean
@Order(DRUID_ORDER)
@ConditionalOnMissingBean
public DruidDataSourceCreator druidDataSourceCreator() {
return new DruidDataSourceCreator(properties.getDruid());
}
}
/**
* 存在Hikari数据源时, 加入创建器
*/
@ConditionalOnClass(HikariDataSource.class)
@Configuration
public class HikariDataSourceCreatorConfiguration {
@Bean
@Order(HIKARI_ORDER)
@ConditionalOnMissingBean
public HikariDataSourceCreator hikariDataSourceCreator() {
return new HikariDataSourceCreator(properties.getHikari());
}
}
}
2.4 DS注解如何被拦截处理的
注解拦截处理离不开AOP,这里介绍代码中如何使用AOP。
我们还是从DynamicDataSourceAutoConfiguration入口配置类中dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor)方法入手,该方法注入了一个DynamicDataSourceAnnotationAdvisor类型的bean对象。
在讲解这个advisor之前,这里多提一点AOP相关的。
在 Spring AOP 中,有 3 个常用的概念:Advices 、 Pointcut 、 Advisor ,解释如下:
Advice :一个 method 执行前或执行后的动作。
Pointcut :根据 method 的名字或者正则表达式等方式,去拦截一个 method 。
Advisor : Advice 和 Pointcut 组成的独立的单元,并且能够传给 proxy factory 对象。
@Component
//声明这是一个切面Bean
@Aspect
public class ServiceAspect {
//配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点
@Pointcut("execution(* com.xxx.aop.service..*(..))")
public void aspect() {
}
/*
* 配置前置通知,使用在方法aspect()上注册的切入点
* 同时接受JoinPoint切入点对象,可以没有该参数
*/
@Before("aspect()")
public void before(JoinPoint joinPoint) {
}
//配置后置通知,使用在方法aspect()上注册的切入点
@After("aspect()")
public void after(JoinPoint joinPoint) {
}
//配置环绕通知,使用在方法aspect()上注册的切入点
@Around("aspect()")
public void around(JoinPoint joinPoint) {
}
//配置后置返回通知,使用在方法aspect()上注册的切入点
@AfterReturning("aspect()")
public void afterReturn(JoinPoint joinPoint) {
}
//配置抛出异常后通知,使用在方法aspect()上注册的切入点
@AfterThrowing(pointcut = "aspect()", throwing = "ex")
public void afterThrow(JoinPoint joinPoint, Exception ex) {
}
}
平时,我们可能使用这种AspectJ注解多一些,通过@Aspect注解的方式来声明切面,spring会通过我们的AspectJ注解(比如@Pointcut、@Before) 动态生成各个Advisor。
Spring还提供了另一种切面----顾问(Advisor),其可以完成更为复杂的切面织入功能。我们可以通过直接继承AbstractPointcutAdvisor来提供切面逻辑,生成对应的Advisor实例,如下图:
其中,最重要的就是getAdvice和getPointcut方法,可以简单认为advisor=advice+pointcut。
再回到DynamicDataSourceAutoConfiguration入口配置类中dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor)方法,跟进观察DynamicDataSourceAnnotationAdvisor类:
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
// 通知
private final Advice advice;
// 切入点
private final Pointcut pointcut;
public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
this.advice = dynamicDataSourceAnnotationInterceptor;
this.pointcut = buildPointcut();
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public Advice getAdvice() {
return this.advice;
}
@Override
public 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);
}
/**
* In order to be compatible with the spring lower than 5.0
*/
private static class AnnotationMethodPoint implements Pointcut {
private final Class<? extends Annotation> annotationType;
public AnnotationMethodPoint(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "Annotation type must not be null");
this.annotationType = annotationType;
}
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new AnnotationMethodMatcher(annotationType);
}
private static class AnnotationMethodMatcher extends StaticMethodMatcher {
private final Class<? extends Annotation> annotationType;
public AnnotationMethodMatcher(Class<? extends Annotation> annotationType) {
this.annotationType = annotationType;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (matchesMethod(method)) {
return true;
}
// Proxy classes never have annotations on their redeclared methods.
if (Proxy.isProxyClass(targetClass)) {
return false;
}
// The method may be on an interface, so let's check on the target class as well.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
return (specificMethod != method && matchesMethod(specificMethod));
}
private boolean matchesMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, this.annotationType);
}
}
}
}
现在看下@DS注解的advisor实现,在buildPointcut方法里拦截了被@DS注解的方法或类,并且使用ComposablePointcut组合切入点,可以实现方法优先级大于类优先级的特性。DynamicDataSourceAnnotationAdvisor通过构造方法传过来的参数类型是DynamicDataSourceAnnotationInterceptor类,跟进观察该类:
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;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String dsKey = determineDatasourceKey(invocation);
// 把获取到的数据源标识(如master)存入本地线程
DynamicDataSourceContextHolder.push(dsKey);
try {
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
// 如果DS注解内容是以#开头,则解析动态最终值;否则,直接返回。
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
这是它的advice通知(也可以说是方法拦截器)执行的动作:在要切换数据源的方法执行前,将“切换的数据源”放入了holder里,等方法执行完后在finally中释放掉,完成当前数据源的切换。该类的determineDatasource()方法决定具体使用哪个数据源。
2.5 多数据源动态切换及如何管理多数据源
在DynamicDataSourceAnnotationInterceptor类中切换数据源的方法中,前后调用了DynamicDataSourceContextHolder.push()和poll()。跟进观察下DynamicDataSourceContextHolder方法:
public final class DynamicDataSourceContextHolder {
/**
* 为什么要用链表存储(准确的是栈)
* <pre>
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
* </pre>
*/
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
@Override
protected Deque<String> initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
* <p>
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
它使用栈处理当前数据源。使用了ArrayDeque这个线程不安全的双端队列来实现栈功能,比原生Stack性能好。使用栈数据结构,嵌套过程中进来push、出去就pop,实现了这个嵌套调用service的业务需求。
下面来看切换数据源的核心类AbstractRoutingDataSource:
该项目没有使用Spring的AbstractRoutingDataSource做多数据源动态切换,而是自定义实现了一个AbstractRoutingDataSource类,如下所示:
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
/**
* 由子类实现,决定最终数据源。
*
* @return 数据源
*/
protected abstract DataSource determineDataSource();
@Override
public Connection getConnection() throws SQLException {
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
return determineDataSource().getConnection();
} else {
String ds = DynamicDataSourceContextHolder.peek();
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
}
}
@Override
public 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;
}
}
private Connection getConnectionProxy(String ds, Connection connection) {
ConnectionProxy connectionProxy = new ConnectionProxy(connection, ds);
ConnectionFactory.putConnection(ds, connectionProxy);
return connectionProxy;
}
@Override
@SuppressWarnings("unchecked")
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this)) {
return (T) this;
}
return determineDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (iface.isInstance(this) || determineDataSource().isWrapperFor(iface));
}
}
该抽象类也是实现了DataSource接口的getConnection方法,现在来看下子类如何实现determineDataSource方法:
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final String UNDERLINE = "_";
/**
* 所有数据库
*/
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
/**
* 分组数据库
*/
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();
@Setter
private DynamicDataSourceProvider provider;
@Setter
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
@Setter
private String primary = "master";
@Setter
private Boolean strict = false;
@Setter
private Boolean p6spy = false;
@Setter
private Boolean seata = false;
@Override
public DataSource determineDataSource() {
return getDataSource(DynamicDataSourceContextHolder.peek());
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}
/**
* 获取当前所有的数据源
*
* @return 当前所有数据源
*/
public Map<String, DataSource> getCurrentDataSources() {
return dataSourceMap;
}
/**
* 获取的当前所有的分组数据源
*
* @return 当前所有的分组数据源
*/
public Map<String, GroupDataSource> getCurrentGroupDataSources() {
return groupDataSources;
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
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);
return dataSourceMap.get(ds);
}
if (strict) {
throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
/**
* 添加数据源
*
* @param ds 数据源名称
* @param dataSource 数据源
*/
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = dataSourceMap.put(ds, dataSource);
// 新数据源添加到分组
this.addGroupDataSource(ds, dataSource);
// 关闭老的数据源
if (oldDataSource != null) {
try {
closeDataSource(oldDataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
}
log.info("dynamic-datasource - load a datasource named [{}] success", ds);
}
/**
* 新数据源添加到分组
*
* @param ds 新数据源的名字
* @param dataSource 新数据源
*/
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
GroupDataSource groupDataSource = groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
groupDataSources.put(group, groupDataSource);
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
/**
* 删除数据源
*
* @param ds 数据源名称
*/
public synchronized void removeDataSource(String ds) {
if (!StringUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
}
if (primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
}
if (dataSourceMap.containsKey(ds)) {
DataSource dataSource = dataSourceMap.remove(ds);
try {
closeDataSource(dataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
if (oldDataSource == null) {
if (log.isWarnEnabled()) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
}
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
/**
* 关闭数据源。
* <pre>
* 从3.2.0开启,如果是原生或使用 DataSourceCreator 创建的数据源会包装成ItemDataSource。
* ItemDataSource保留了最原始的数据源,其可直接关闭。
* 如果不是DataSourceCreator创建的数据源则只有尝试解包装再关闭。
* </pre>
*/
private void closeDataSource(DataSource dataSource) throws Exception {
if (dataSource instanceof ItemDataSource) {
((ItemDataSource) dataSource).close();
} else {
if (seata && dataSource instanceof DataSourceProxy) {
DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
dataSource = dataSourceProxy.getTargetDataSource();
}
if (p6spy && dataSource instanceof P6DataSource) {
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
realDataSourceField.setAccessible(true);
dataSource = (DataSource) realDataSourceField.get(dataSource);
}
Class<? extends DataSource> clazz = dataSource.getClass();
Method closeMethod = clazz.getDeclaredMethod("close");
closeMethod.invoke(dataSource);
}
}
@Override
public void destroy() throws Exception {
log.info("dynamic-datasource start closing ....");
for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
closeDataSource(item.getValue());
}
log.info("dynamic-datasource all closed success,bye");
}
@Override
public void afterPropertiesSet() throws Exception {
// 检查开启了配置但没有相关依赖
checkEnv();
// 添加并分组数据源
Map<String, DataSource> dataSources = provider.loadDataSources();
for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
addDataSource(dsItem.getKey(), dsItem.getValue());
}
// 检测默认数据源是否设置
if (groupDataSources.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), primary);
} else if (dataSourceMap.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
} else {
throw new RuntimeException("dynamic-datasource Please check the setting of primary");
}
}
private void checkEnv() {
if (p6spy) {
try {
Class.forName("com.p6spy.engine.spy.P6DataSource");
log.info("dynamic-datasource detect P6SPY plugin and enabled it");
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource enabled P6SPY ,however without p6spy dependency", e);
}
}
if (seata) {
try {
Class.forName("io.seata.rm.datasource.DataSourceProxy");
log.info("dynamic-datasource detect ALIBABA SEATA and enabled it");
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource enabled ALIBABA SEATA,however without seata dependency", e);
}
}
}
}
之前已经将creator生成的数据源连接池放入map中,现在从map中获取数据源即可,可以发现数据源组优先于单数据源。
2.6 组数据源的负载均衡怎么实现的
跟进观察DynamicRoutingDataSource的getDataSource方法:
else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return groupDataSources.get(ds).determineDataSource();
}
如果数据源组不为空,并且DS注解写的数据源组名存在,那么就会通过GroupDataSource类的
determineDataSource方法在数据源组中选取一个数据源。
@Data
public class GroupDataSource {
private String groupName;
// 数据源切换策略
private DynamicDataSourceStrategy dynamicDataSourceStrategy;
private Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();
public GroupDataSource(String groupName, DynamicDataSourceStrategy dynamicDataSourceStrategy) {
this.groupName = groupName;
this.dynamicDataSourceStrategy = dynamicDataSourceStrategy;
}
/**
* add a new datasource to this group
*
* @param ds the name of the datasource
* @param dataSource datasource
*/
public DataSource addDatasource(String ds, DataSource dataSource) {
return dataSourceMap.put(ds, dataSource);
}
/**
* @param ds the name of the datasource
*/
public DataSource removeDatasource(String ds) {
return dataSourceMap.remove(ds);
}
// 根据切换策略,获取一个数据源。
public DataSource determineDataSource() {
return dynamicDataSourceStrategy.determineDataSource(new ArrayList<>(dataSourceMap.values()));
}
public int size() {
return dataSourceMap.size();
}
}
GroupDataSource使用策略模式来决定一个数据源,目前实现的策略有两种:随机和轮询,默认是轮询。在DynamicDataSourceProperties属性中写了默认值,也可以通过配置文件配置。
public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
/**
* 负载均衡计数器
*/
private final AtomicInteger index = new AtomicInteger(0);
@Override
public DataSource determineDataSource(List<DataSource> dataSources) {
return dataSources.get(Math.abs(index.getAndAdd(1) % dataSources.size()));
}
}
如果想通过jdbc获取数据源,该项目有个自定义的抽象类AbstractJdbcDataSourceProvider,需要实现其executeStmt方法,即:从其他数据库查询出url、username、password等信息(就是在yml配置的属性),然后拼接成一个配置对象DataSourceProperty去调用createDataSourceMap方法。
2.7 如何动态增减数据源
这个也是很实用的功能,其实现还是通过DynamicRoutingDataSource这个核心动态数据源组件完成。
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
省略
/**
* 删除数据源
*
* @param ds 数据源名称
*/
public synchronized void removeDataSource(String ds) {
if (!StringUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
}
if (primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
}
if (dataSourceMap.containsKey(ds)) {
DataSource dataSource = dataSourceMap.remove(ds);
try {
closeDataSource(dataSource);
} catch (Exception e) {
log.error("dynamic-datasource - remove the database named [{}] failed", ds, e);
}
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
if (oldDataSource == null) {
if (log.isWarnEnabled()) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
}
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
省略
}
可以发现,该项目预留了相关接口给开发者,以方便添加、删除数据库。
添加数据源的步骤:
1、注入DynamicRoutingDataSource和DataSourceCreator。
2、通过数据源配置(url、username、password等)构建一个DataSourceProperty对象。
3、通过dataSourceCreator,根据配置属性构建一个真实的DataSource。
4、调用DynamicRoutingDataSource的addDataSource方法添加这个DataSource。
同理,删除数据源的步骤:
1、注入DynamicRoutingDataSource。
2、调用DynamicRoutingDataSource的removeDataSource方法。
@PostMapping("/add")
@ApiOperation("通用添加数据源(推荐)")
public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}
@DeleteMapping
@ApiOperation("删除数据源")
public String remove(String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.removeDataSource(name);
return "删除成功";
}
三、总结
通过阅读该项目源码,熟悉了spring aop、spring事务管理、spring boot自动配置等spring知识点,可以根据业务需求去进一步扩展这个starter。