[TOC]
1. 使用
1.1. 引包
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.3.6</version>
</dependency>
用最新版的就好 Maven Repository: dynamic-datasource-spring-boot-starter
不过做开发的经验来看, 应该选 倒数第二个大版本里最新的版本, 这样既能用更多的功能,又能保证没有大bug, 除非最新版本有你需要的功能
1.2 增加配置
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master, 所以此处不写也可以
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
datasource:
master: # 第一个数据源的名称
url: jdbc:mysql://127.0.0.1:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
db_1: # 第二个数据源的名称
url: jdbc:gbase://127.0.0.1:5258/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
如果是多主多从或者一主多从,那么就用分组的功能实现, 即: 数据组名称_xxx,下划线前面的就是数据组名称,后面就是数据源名, 相同组名称的数据源会放在一个组下。切换数据源时,可以指定具体数据源名称,也可以指定组名然后会自动采用负载均衡算法切换,
例如: 上面配置中的"db_1", 表示db分组, 1 名称的数据源, 如果再增加一个"db_2", 则表示这两个数据源是同一个分组, 当在@DS
中只指定"db", 则会在这个分组中采用负载均衡方式选数据源(默认是轮询)
1.3 使用@DS注解
@DS注解, 是用来表示使用哪个数据源的, 也是切换数据源功能的核心注解, 它可以用来类和方法上, 方法上的优先级更高, 例如 service层, Mapper层,都可以, 如果没有配置,则会使用默认数据源
@Service
@DS("master") // 在类上加
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("db_1") // 在方法上加
public List<Map<String, Object>> selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
官方建议不建议@DS放在Mapper上, 所以网上有很多例子是放在service上,
但是这有个问题, 当在service中需要访问两个数据库时, 就会无法切换数据源, 毕竟@DS是加在service上, 在同一个service上操作, 自然不会切换, 官方也给出解决方案, 在方法上进一步指定数据源, (伪代码)例如:
class Person {
@Autowired
dbService1 db_1;
@Autowired
dbService2 db_2;
void test(){
db_1.save();
// 如果 db_2.save() 不指定数据源, 数据源会使用db_1
db_2.save();
}
}
@DS("db_1")
class dbService1 {
void save(){
}
}
@DS("db_2")
class dbService2 {
@DS("db_2")
void save(){
}
}
mybatis-plus中多数据源切换@DS注解到底放在哪一层合适?
2. 源码
一般starter自动配置,都是从 META-INF/spring.factories文件中指定自动配置类:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
所以打开 DynamicDataSourceAutoConfiguration 文件
2.1 Configuration文件, 加载配置与bean注入
// 动态数据源核心自动配置类
@Slf4j
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
// @AutoConfigureBefore 注解的意思是 在DataSourceAutoConfiguration和DruidDataSourceAutoConfigure之前就注册当前bean
// 这里特别注意一下, 在 V3.3.3 之前, 没有把DruidDataSourceAutoConfigure加进来, 所以导致项目中有druid时,数据源无法切换或没有druid配置则报错
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//引入了Druid的autoConfig和各种数据源连接池的Creator
@Import(value = {DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceAopConfiguration.class, DynamicDataSourceAssistConfiguration.class})
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {
private final DynamicDataSourceProperties properties;
//读取多数据源配置,注入到spring容器中
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
Map<String, DataSourceProperty> datasourceMap = properties.getDatasource();
return new YmlDynamicDataSourceProvider(datasourceMap);
}
//注册自己的动态多数据源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注解过的方法进行增强,达到切换数据源的目的
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}
//关于分布式事务加强
@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());
}
//动态参数解析器链
@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;
}
}
我们可以发现,在使用的时候配置的前缀为spring.datasource.dynamic
的配置都会被读取到DynamicDataSourceProperties
类,作为一个Bean注入到Spring容器。其实这种读取配置文件信息的方式在日常开发中也是很常见的。
2.1.1 配置类
DynamicDataSourceProperties
@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;
// .......
}
2.2 注册DataSource
在 DynamicDataSourceAutoConfiguration
类中, 还加载了DataSource, 我们知道 DataSource是spring用来链接数据库的,
它注册了一个实现类 DynamicRoutingDataSource
, 这个类还有一个父类AbstractRoutingDataSource
, 我们先看AbstractRoutingDataSource
/**
* 抽象动态获取数据源
*
* @author TaoYu
* @since 2.2.0
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource {
//抽象方法,由子类实现,让子类决定最终使用的数据源
protected abstract DataSource determineDataSource();
//重写getConnection()方法,实现切换数据源的功能
@Override
public Connection getConnection() throws SQLException {
//这里xid涉及分布式事务的处理
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
//不使用分布式事务,就是直接返回一个数据连接(实现类: ItemDataSource.class)
return determineDataSource().getConnection();
} else {
String ds = DynamicDataSourceContextHolder.peek();
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
}
}
}
父类很简单, 只是区分了分布式事务的取connect的方式,所以再来看下其子类DynamicRoutingDataSource
的内容
@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);
}
// bean的属性加载完后, 执行该方法
@Override
public void afterPropertiesSet() throws Exception {
// 检查开启了配置但没有相关依赖
checkEnv();
// 拿到所有的数据源, Key是数据源的名称,Value则是DataSource。
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");
}
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
// 没有ds注解, 则默认用 Primary
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 CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
// .....
}
它实现了InitializingBean
接口,这个接口需要实现afterPropertiesSet()
方法,这是一个Bean的生命周期函数,在Bean初始化的时候做一些操作
2.2.1 获取所有的DataSource
先看一下它是怎么获得数据源的? provider.loadDataSources()
是抽象方法, 有两个实现
YmlDynamicDataSourceProvider
和AbstractJdbcDataSourceProvider
, 在DynamicDataSourceAutoConfiguration
类中, 注入的是YmlDynamicDataSourceProvider
, 所以直接看它的实现 , 最后会跳转到AbstractDataSourceProvider
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
@Autowired
private DefaultDataSourceCreator defaultDataSourceCreator;
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
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);
// 这里的defaultDataSourceCreator.createDataSource()方法使用到适配器模式。因为每种配置数据源创建的DataSource实现类都不一定相同的,所以需要根据配置的数据源类型进行具体的DataSource创建。
dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
2.2.1.1 构建DataSource
来看一下defaultDataSourceCreator.createDataSource()
的逻辑
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DataSourceCreator dataSourceCreator = null;
//this.creators是所有适配的DataSourceCreator实现类
for (DataSourceCreator creator : this.creators) {
//根据配置匹配对应的dataSourceCreator
if (creator.support(dataSourceProperty)) {
//如果匹配,则使用对应的dataSourceCreator
dataSourceCreator = creator;
break;
}
}
if (dataSourceCreator == null) {
throw new IllegalStateException("creator must not be null,please check the DataSourceCreator");
}
String publicKey = dataSourceProperty.getPublicKey();
if (StringUtils.isEmpty(publicKey)) {
dataSourceProperty.setPublicKey(properties.getPublicKey());
}
Boolean lazy = dataSourceProperty.getLazy();
if (lazy == null) {
dataSourceProperty.setLazy(properties.getLazy());
}
//然后再调用createDataSource方法进行创建对应DataSource
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
// (如果有则运行, 可以自定义)运行 建表语句和数据脚本
this.runScrip(dataSource, dataSourceProperty);
return wrapDataSource(dataSource, dataSourceProperty);
}
创建DataSource的有很多实现类, 每种数据源都有自己的实现
对应的全部实现类是放在creator包下:
创建dataSource就很简单了, 就是把账号/密码/url等参数设置进去
2.2.2 对数据源分组
再回到之前的,当拿到DataSource的Map集合之后,再做什么呢?
接着调addDataSource()
方法,这个方法是根据下划线"_“对数据源进行分组,最后放到groupDataSources
成员变量里面。 com.baomidou.dynamic.datasource.DynamicRoutingDataSource#addDataSource
private static final String UNDERLINE = "_";
// 轮询策略
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
/**
* 新数据源添加到分组
*
* @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 {
//顺便设置负载均衡策略,strategy默认是LoadBalanceDynamicDataSourceStrategy
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);
}
}
至此, 项目启动时, 多数据做的事就完了, 那我们执行sql时, 它是怎么切换的呢?
2.3 切换数据源
在前面的DynamicRoutingDataSource
我们知道, 这是mybatisPlus写的获取数据源的类 , 它最终是通过com.baomidou.dynamic.datasource.DynamicRoutingDataSource#determineDataSource
获得数据源的, 在 2.2.1 也有描述.
@Override
public DataSource determineDataSource() {
// 从threadLocal中获取当前数据源key (就是@DS注解里的字符串)
String dsKey = DynamicDataSourceContextHolder.peek();
// 根据key获取数据源
return getDataSource(dsKey);
}
那么上面的DynamicDataSourceContextHolder
这个类是干嘛的呢?注解@DS的值又是怎么传进来的呢?
我们先看看这个类
2.3.1 DynamicDataSourceContextHolder
/**
* 核心基于ThreadLocal的切换数据源工具类
*/
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<>();
}
};
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
*/
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
// ....
}
这里用的是NamedThreadLocal
, 比ThreadLocal
多了一个名字字段而已, 大家知道ThreadLocal
的特性: 线程隔离/内存泄露风险, 所以 当开多线程会让多数据源切换失败(看你@DS注解打在哪和具体使用); 网上说用到 DynamicDataSourceContextHolder
类时, 都是push()
和clean()
/poll()
一起使用.其实就是避免内存泄露
既然有peek()
, 那肯定有push()
, 我们来反推一下, 看一下哪些地方在调用push()
, 可以发现有两个地方, 分别是
DynamicDataSourceAnnotationInterceptor
和 MasterSlaveAutoRoutingPlugin
,
MasterSlaveAutoRoutingPlugin
是做主从数据库切换的, 我们自然看 DynamicDataSourceAnnotationInterceptor
2.3.2 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 {
//找到@DS注解的属性值,也就是数据源名称
String dsKey = determineDatasourceKey(invocation);
//把数据源名称push到当前线程的栈
DynamicDataSourceContextHolder.push(dsKey);
try {
//执行当前方法
return invocation.proceed();
} finally {
//从栈里释放数据源
DynamicDataSourceContextHolder.poll();
}
}
private String determineDatasourceKey(MethodInvocation invocation) {
// 从方法上和类上 获取key
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
// 如果数据源是以 # 开头, 则进入ds处理器, 这里用的是责任链模式
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
它是invoke方法, 一看就是反射标准写法, 看一下它的父类, MethodInterceptor
, 它可以对AOP的切面进行增强,
简单来说, 如果被切到后, 就会调用这个invoke方法
我们继续看切面是如何使用的, 而ds处理器等会讲
2.3.4 DynamicDataSourceAnnotationAdvisor
直接看 DynamicDataSourceAnnotationInterceptor
在哪里用到了, 会发现回到了DynamicDataSourceAutoConfiguration
类
@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@Bean
public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
// 创建拦截器
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);
// 把拦截器放到Advisor上
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(properties.getOrder());
return advisor;
}
Advisor是Spring AOP的顶层抽象,用来管理Advice和Pointcut
@Transactional 也是用这种方式做的
从 AbstractPointcutAdvisor 开始: Spring AOP 之 Advisor、PointcutAdvisor 介绍
DynamicDataSourceAnnotationAdvisor
是用于AOP切面编程的,针对注解@DS的切面进行处理:
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;
}
private Pointcut buildPointcut() {
Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
Pointcut mpc = new AnnotationMethodPoint(DS.class);
//方法优于类
return new ComposablePointcut(cpc).union(mpc);
}
// .....
}
所以总的来说, 当执行的方法或者类被 @DS切到时, 就会执行增强方法, 把数据源key放到threadLocal中, 获取数据源时从threadLocal中拿到key进而拿到数据源
至此, 多数据源的初始化和使用时切换就结束了
2.4 数据源的处理器
在com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor#determineDatasourceKey
处, 有说过ds处理器, 它是干什么的呢?
我们常用的指定数据源的方式时是 @DS("master")
@DS("salve_1")
, 这些都是固定字符串, 如果我们需要根据租户来切换数据源呢?或者引入灰度系统后按Header的属性来切换呢? 因此plus的提供了各种数据源的处理器
- DsHeaderProcessor : 从header中获取
- DsSessionProcessor: 从session中获取
- DsSpelExpressionProcessor: 解析Spel语法获取
常见的@vaule()注解就支持spel语法, 从配置文件中取值或者写默认值都是spel语法
继续 com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor#determineDatasourceKey
, 看一下它的代码
private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
// dsProcessor.determineDatasource() 从处理器获取数据源
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
dsProcessor
反向定位这个变量, 会追踪到com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration#dsProcessor
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor(BeanFactory beanFactory) {
// 先header处理器
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
// 再session处理器
headerProcessor.setNextProcessor(sessionProcessor);
// 最后spel处理器
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
回到使用的地方, 进入方法
com.baomidou.dynamic.datasource.processor.DsProcessor
/**
* 决定数据源
* <pre>
* 调用底层doDetermineDatasource,
* 如果返回的是null则继续执行下一个,否则直接返回
* </pre>
*
* @param invocation 方法执行信息
* @param key DS注解里的内容
* @return 数据源名称
*/
public String determineDatasource(MethodInvocation invocation, String key) {
// key的格式是否匹配
if (matches(key)) {
// 解析数据源key
String datasource = doDetermineDatasource(invocation, key);
if (datasource == null && nextProcessor != null) {
// 下一个处理器
return nextProcessor.determineDatasource(invocation, key);
}
return datasource;
}
if (nextProcessor != null) {
return nextProcessor.determineDatasource(invocation, key);
}
return null;
}
我们以DsHeaderProcessor
为例:
public class DsHeaderProcessor extends DsProcessor {
/**
* header prefix
*/
private static final String HEADER_PREFIX = "#header";
@Override
public boolean matches(String key) {
// 是否 以#header 开头
return key.startsWith(HEADER_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 从请求头中直接获取(很粗暴,竟然连分隔符都不用), 比如@DS("#headeruserId"), 这样就会从header中取出userId字段
return request.getHeader(key.substring(8));// 刚好把 #header 这几个字符截断
}
}
session处理器是以#session
开头
按顺序处理都不是后, 就会按DsSpelExpressionProcessor
处理
举一些例子:
@DS("#headerUserId")
: 取header中的UserId字段作为数据源key
@DS("#sessionTentendId")
: 取session中的TentendId字段作为数据源key
@DS("#person")
: 把方法上的变量person的值作为数据源key (SPEL语法)
学习SPEL语法就会发现很多奇妙用法, 比如可与获取方法的参数;方法的返参类型; 类的名称等
3. Q&A
3.1 @Transactional 和 @DS 注解一起使用时, @DS失效的问题
这是因为 @Transactional
开启事务后, 就不会重新拿数据源,因为@DS也得通过切面去获取数据源, 这样就导致了@DS失效.
要解决的话, 就在要切换数据源的方法上也打上@DS, 或者多个数据源有修改操作可以都打上事务注解并改变传播机制,(但这其实是分布式事务的范畴, 这样操作不能保证事务了, plus也提供了@DSTransactional
来支持, 不够需要借助seata
)
@Transactional跟@DS动态数据源注解冲突_林蜗牛snail的博客-CSDN博客
关于
@DSTransactional
以后再补充一下
3.2 低于V3.3.3版本无法切换数据源的问题
这是因为在V3.3.3版本之前, DruidDataSourceAutoConfigure
会比DynamicDataSourceAutoConfiguration
先注册, 导致数据库有读取druid
的配置, 所以会出现 ,没有druid配置启动报错; 数据源无法切换等等原因, 官方在V3.3.3这个版本修复了, 但是这个版本有个其他的重点bug,官方不让下载, 所以github上找不到这个版本的描述, 只在官网有描述
分两步解决的,
- 去除 druid pom坐标
- 在
DynamicDataSourceAutoConfiguration
上指定优于DruidDataSourceAutoConfigure
加载
解决办法就是排除DruidDataSourceAutoConfigure
类;
使用dynamic-datasource-spring-boot-starter做多数据源及源码分析_0x2015的博客-CSDN博客