写在前面,发现问题
在现在的环境中,开发时经常遇到一个程序需要同时访问多个数据库的情况,MyBatis-Plus提供了快捷方式的配置方式。如在yaml配置文件里配置多个数据源db1、db2、db3,当需要使用哪个数据源时只需要在想要使用的类或者方法上加上@DS("db1")、@DS("db2")、@DS("db3“)注解,轻松实现数据源的切换。如果没有指定数据源,默认使用master数据源,即配置里的第一个。
使用过程中遇到了一个问题,在需要使用其他数据源的Mapper接口声明了@DS("slave")访问slave数据库,但是在动态sql构造器执行sql时,仍然访问master主数据源,于是乎就报错 表或视图不存在。
public void AService{
@Resourece
private AMapper a;
@Resourece
private BMapper b;
void test(){
//访问master数据源
a.selectA();
//访问slave数据源
b.selectB();
//访问master数据源,同时报错表或试图不存在
b.list(new LambdaQueryWrapper().eq("id",1));
}
}
@DS("master")
public Interface AMapper{
void selectA();
}
@DS("slave")
public Interface BMapper extend BaseMapper<B>{
void selectB();
}
解读源码,询找答案
1. pom文件引用
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
2. 自动配置类
在jar包中找到 META-INF/spring.factories,一般里面指定自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration
打开DynamicDataSourceAutoConfiguration类
@Configuration
@EnableConfigurationProperties({DynamicDataSourceProperties.class})
//spring在装配DataSourceAutoConfiguration的bean之前,先装配当前类bean
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
//引用了Druid的动态数据源配置
@Import({DruidDynamicDataSourceConfiguration.class})
//条件加载,配置文件的前缀是"spring.datasource.dynamic"时启动这个类
@ConditionalOnProperty(
prefix = "spring.datasource.dynamic",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public class DynamicDataSourceAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceAutoConfiguration.class);
@Autowired
private DynamicDataSourceProperties properties;
@Autowired(
required = false
)
private ApplicationContext applicationContext;
public DynamicDataSourceAutoConfiguration() {
}
//读取多数据源的配置,注入到Spring容器中
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
return new YmlDynamicDataSourceProvider(this.properties);
}
//创建连接池的适配器
@Bean
@ConditionalOnMissingBean
public DataSourceCreator dataSourceCreator() {
DataSourceCreator dataSourceCreator = new DataSourceCreator();
dataSourceCreator.setDruidDataSourceCreator(new DruidDataSourceCreator(this.properties.getDruid(), this.applicationContext));
dataSourceCreator.setHikariDataSourceCreator(new HikariDataSourceCreator(this.properties.getHikari()));
dataSourceCreator.setGlobalPublicKey(this.properties.getPublicKey());
return dataSourceCreator;
}
//注册动态数据源DataSource
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(this.properties.getPrimary());
dataSource.setStrategy(this.properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(this.properties.getP6spy());
dataSource.setStrict(this.properties.getStrict());
dataSource.setSeata(this.properties.getSeata());
return dataSource;
}
//注册使用Cglib方式实现动态代理,完成AOP切面,从而达到切换数据源的目的
@Bean
@ConditionalOnMissingBean
public DynamicDataSourceAnnotationAdvisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor();
interceptor.setDsProcessor(dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor);
advisor.setOrder(this.properties.getOrder());
return advisor;
}
//注册spel表达式,动态参数解析器链
@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;
}
//使用spel方式来切换数据源(实验性功能)
@Bean
@ConditionalOnBean({DynamicDataSourceConfigure.class})
public DynamicDataSourceAdvisor dynamicAdvisor(DynamicDataSourceConfigure dynamicDataSourceConfigure, DsProcessor dsProcessor) {
DynamicDataSourceAdvisor advisor = new DynamicDataSourceAdvisor(dynamicDataSourceConfigure.getMatchers());
advisor.setDsProcessor(dsProcessor);
advisor.setOrder(-2147483648);
return advisor;
}
}
以上的几个bean的配置便是Mybatis-Plus的核心配置了。
3. 数据源配置
可以看出,配置文件中是以"spring.datasource.dynamic"为前缀读取配置信息
@ConfigurationProperties(
prefix = "spring.datasource.dynamic"
)
public class DynamicRoutingDataSourceDynamicDataSourceProperties {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceProperties.class);
public static final String PREFIX = "spring.datasource.dynamic";
public static final String HEALTH = "spring.datasource.dynamic.health";
// 默认的库是master
private String primary = "master";
//严格模式:true时未匹配数据源直接报错,false时未匹配数据源采用primary数据源
private Boolean strict = false;
private Boolean p6spy = false;
private Boolean seata = false;
//是否使用spring actuator监控检查,默认不检查
private boolean health = false;
//存储数据源
private Map<String, DataSourceProperty> datasource = new LinkedHashMap();
//多数据源的选择算法,默认负载均衡
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
//AOP切面的顺序,默认优先级最高
private Integer order = -2147483648;
//Druid全局参数配置
@NestedConfigurationProperty
private DruidConfig druid = new DruidConfig();
//Hikari全局参数配置
@NestedConfigurationProperty
private HikariCpConfig hikari = new HikariCpConfig();
//全局默认的公钥
private String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJ4o6sn4WoPmbs7DR9mGQzuuUQM9erQTVPpwxIzB0ETYkyKffO097qXVRLA6KPmaV+/siWewR7vpfYYjWajw5KkCAwEAAQ==";
public DynamicDataSourceProperties() {
}
...
}
可以发现,所有在spring.datasource.dynamic配置的内容都是注入到这个bean中,其中在创建Druid和Hikari连接池的全局参数配置时,套用了@NestedConfigurationProperty注解,这是嵌套了又一层的复杂注解。分别点击进入配置类,就可以看到可以配置的更详细的配置项。
4. 注册动态数据源DataSource
在注册动态数据源DataSource时,创建了DynamicRoutingDataSource对象,而DynamicRoutingDataSource继承自AbstractRoutingDataSource抽象类,实现了InitializingBean接口。 关注1.重写的父类的抽象方法,2实现接口定义的方法。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
private static final String UNDERLINE = "_";
private DynamicDataSourceProvider provider;
private Class<? extends DynamicDataSourceStrategy> strategy;
private String primary;
private boolean strict;
private boolean p6spy;
private boolean seata;
//所有数据库
private Map<String, DataSource> dataSourceMap = new LinkedHashMap();
//分组数据库
private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap();
public DynamicRoutingDataSource() {
}
//继承自父类的方法重写:决定选择数据源,看到peek()方法可以联想到这是一个队列
public DataSource determineDataSource() {
return this.getDataSource(DynamicDataSourceContextHolder.peek());
}
...
//真正决定选择数据源的地方,无指定则首选,有指定则指定,严格模式未匹配报错,非严格模式未匹配首选
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return this.determinePrimaryDataSource();
} else if (!this.groupDataSources.isEmpty() && this.groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return ((DynamicGroupDataSource)this.groupDataSources.get(ds)).determineDataSource();
} else if (this.dataSourceMap.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return (DataSource)this.dataSourceMap.get(ds);
} else if (this.strict) {
throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
} else {
return this.determinePrimaryDataSource();
}
}
//实现接口定义的方法:bean实例化后的属性赋值,获取关于dataSources的map集合,key是数据源名称,value是DataSource
public void afterPropertiesSet() throws Exception {
Map<String, DataSource> dataSources = this.provider.loadDataSources();
Iterator var2 = dataSources.entrySet().iterator();
while(var2.hasNext()) {
Entry<String, DataSource> dsItem = (Entry)var2.next();
//addDataSource添加数据源
this.addDataSource((String)dsItem.getKey(), (DataSource)dsItem.getValue());
}
if (this.groupDataSources.containsKey(this.primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary group datasource named [{}]", dataSources.size(), this.primary);
} else {
if (!this.dataSourceMap.containsKey(this.primary)) {
throw new RuntimeException("dynamic-datasource Please check the setting of primary");
}
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
}
}
...
}
动态数据源注册到Map集合中,有需要用的时候再从Map中取,在初始化bean获取Map的过程中用到了负载均衡的算法,下章再看provider构造器。同时DynamicDataSourceContextHolder.peek()方法也引人注目。
5. 注册数据源的构造器DynamicDataSourceProvider
public interface DynamicDataSourceProvider {
Map<String, DataSource> loadDataSources();
}
这是一个构造器的抽象接口,在查找实现类的时候发现了有两个。一个是JDBC的实现类,用于数据库的连接。另一个是Yml的实现类,用于创建上文中的数据源Map,此处重点关注创建。
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider implements DynamicDataSourceProvider {
private static final Logger log = LoggerFactory.getLogger(YmlDynamicDataSourceProvider.class);
private DynamicDataSourceProperties properties;
public YmlDynamicDataSourceProvider(DynamicDataSourceProperties properties) {
this.properties = properties;
}
public Map<String, DataSource> loadDataSources() {
Map<String, DataSourceProperty> dataSourcePropertiesMap = this.properties.getDatasource();
return this.createDataSourceMap(dataSourcePropertiesMap);
}
}
调用的创建方式在父类中,继续看父类
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {
private static final Logger log = LoggerFactory.getLogger(AbstractDataSourceProvider.class);
@Autowired
private DataSourceCreator dataSourceCreator;
public AbstractDataSourceProvider() {
}
protected Map<String, DataSource> createDataSourceMap(Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap(dataSourcePropertiesMap.size() * 2);
Iterator var3 = dataSourcePropertiesMap.entrySet().iterator();
while(var3.hasNext()) {
Entry<String, DataSourceProperty> item = (Entry)var3.next();
DataSourceProperty dataSourceProperty = (DataSourceProperty)item.getValue();
String pollName = dataSourceProperty.getPollName();
if (pollName == null || "".equals(pollName)) {
pollName = (String)item.getKey();
}
dataSourceProperty.setPollName(pollName);
dataSourceMap.put(pollName, this.dataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
跟踪又发现,createDataSourceMap方法中调用的是dataSourceCreator.createDataSource(dataSourceProperty)
6. 创建连接池的适配器
这里的createDataSource方法使用了适配器模式,每种配置数据源创建的方式不尽相同,根据配置中的type或者连接池的class来判定该创建什么类型的连接池。
public class DataSourceCreator {
private static final Logger log = LoggerFactory.getLogger(DataSourceCreator.class);
private static Boolean druidExists = false;
private static Boolean hikariExists = false;
private HikariDataSourceCreator hikariDataSourceCreator;
private DruidDataSourceCreator druidDataSourceCreator;
private String globalPublicKey;
public DataSourceCreator() {
}
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
String jndiName = dataSourceProperty.getJndiName();
DataSource dataSource;
if (jndiName != null && !jndiName.isEmpty()) {
dataSource = this.createJNDIDataSource(jndiName);
} else {
Class<? extends DataSource> type = dataSourceProperty.getType();
if (type == null) {
if (druidExists) {
dataSource = this.createDruidDataSource(dataSourceProperty);
} else if (hikariExists) {
dataSource = this.createHikariDataSource(dataSourceProperty);
} else {
dataSource = this.createBasicDataSource(dataSourceProperty);
}
} else if ("com.alibaba.druid.pool.DruidDataSource".equals(type.getName())) {
dataSource = this.createDruidDataSource(dataSourceProperty);
} else if ("com.zaxxer.hikari.HikariDataSource".equals(type.getName())) {
dataSource = this.createHikariDataSource(dataSourceProperty);
} else {
dataSource = this.createBasicDataSource(dataSourceProperty);
}
}
this.runScrip(dataSourceProperty, dataSource);
return dataSource;
}
...
}
7. 数据源的新增
在注册动态数据源DataSource时,初始化过程中,先创建了连接池,此时回头再看如何加入数据源Map中的。
//递归的方式创建数据源Map和分组
public synchronized void addDataSource(String ds, DataSource dataSource) {
if (this.p6spy) {
dataSource = new P6DataSource((DataSource)dataSource);
log.info("dynamic-datasource [{}] wrap p6spy plugin", ds);
}
if (this.seata) {
dataSource = new DataSourceProxy((DataSource)dataSource);
log.info("dynamic-datasource [{}] wrap seata plugin", ds);
}
this.dataSourceMap.put(ds, dataSource);
if (ds.contains("_")) {
String group = ds.split("_")[0];
if (this.groupDataSources.containsKey(group)) {
((DynamicGroupDataSource)this.groupDataSources.get(group)).addDatasource((DataSource)dataSource);
} else {
try {
DynamicGroupDataSource groupDatasource = new DynamicGroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.newInstance());
groupDatasource.addDatasource((DataSource)dataSource);
this.groupDataSources.put(group, groupDatasource);
} catch (Exception var5) {
log.error("dynamic-datasource - add the datasource named [{}] error", ds, var5);
this.dataSourceMap.remove(ds);
}
}
}
log.info("dynamic-datasource - load a datasource named [{}] success", ds);
}
8. DynamicDataSourceContextHolder的思考
上文提到获取数据源是获取DynamicDataSourceContextHolder.peek(),用于获取数据源
public final class DynamicDataSourceContextHolder {
//这是一个链表存储,为什么要用链表存储
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
protected Object initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
//获取当前线程的的数据源
public static String peek() {
return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
}
//设置当前线程的数据源
public static void push(String ds) {
((Deque)LOOKUP_KEY_HOLDER.get()).push(StringUtils.isEmpty(ds) ? "" : ds);
}
//清空当前线程的数据源
public static void poll() {
Deque<String> deque = (Deque)LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
//强制清空所有数据源
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
这里为什么使用栈?
9. DynamicDataSourceAnnotationAdvisor切面编程,DS注解切换
注解拦截处理离不开AOP,利用Cglib完成对动态代理。
Spring AOP的概念
Advices:描述需要在方法执行的什么时机织入代码
PointCut:描述在哪些类的哪些方法执行织入代码
Advisor:说明这个类是切面组成的类,由Advice和Pointcut两部分组成
- Advisor 切面
public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
//通知
private Advice advice;
//切点
private Pointcut pointcut;
//动态代理织入
public DynamicDataSourceAnnotationAdvisor(@NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {
if (dynamicDataSourceAnnotationInterceptor == null) {
throw new NullPointerException("dynamicDataSourceAnnotationInterceptor");
} else {
this.advice = dynamicDataSourceAnnotationInterceptor;
this.pointcut = this.buildPointcut();
}
}
public Pointcut getPointcut() {
return this.pointcut;
}
public Advice getAdvice() {
return this.advice;
}
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 = AnnotationMatchingPointcut.forMethodAnnotation(DS.class);
//方法的优先级更高
return (new ComposablePointcut(cpc)).union(mpc);
}
}
- PointCut 切点,被用@DS注解的类和方法会作为切点
//作用在类上和在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
//注解在运行时一直保留
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
String value();
}
- Advices 通知
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {
private static final String DYNAMIC_PREFIX = "#";
private static final DynamicDataSourceClassResolver RESOLVER = new DynamicDataSourceClassResolver();
private DsProcessor dsProcessor;
public DynamicDataSourceAnnotationInterceptor() {
}
public Object invoke(MethodInvocation invocation) throws Throwable {
Object var2;
try {
//找到执行的数据源,并推入栈中
DynamicDataSourceContextHolder.push(this.determineDatasource(invocation));
//执行当前方法
var2 = invocation.proceed();
} finally {
//数据源出栈
DynamicDataSourceContextHolder.poll();
}
return var2;
}
//责任链模式的处理
private String determineDatasource(MethodInvocation invocation) throws Throwable {
//获取DS注解的内容
Method method = invocation.getMethod();
DS ds = method.isAnnotationPresent(DS.class) ? (DS)method.getAnnotation(DS.class) : (DS)AnnotationUtils.findAnnotation(RESOLVER.targetClass(invocation), DS.class);
//获取DS注解的内容
String key = ds.value();
//如果DS注解内容以#开头解析动态最终值,否则直接返回
return !key.isEmpty() && key.startsWith("#") ? this.dsProcessor.determineDatasource(invocation, key) : key;
}
public void setDsProcessor(DsProcessor dsProcessor) {
this.dsProcessor = dsProcessor;
}
}
10. 切换的场景
默认master为主数据源,slave的第二数据源,非严格模式。
-
AService无注解==》访问master数据源
-
AService无注解,AMethod注解DS("slave")==》访问slave数据源
-
AService无注解,AMapper注解DS("slave")==》访问slave数据源
-
AService无注解,BSerivice注解DS("slave"),AService中方法调用BService的方法=》访问slave数据源,B类方法出栈后重新访问master数据源。
-
AService无注解,BMapper注解DS("slave"),AService中方法调用BMapper的方法=》访问slave数据源,B类方法出栈后重新访问master数据源。
找到答案,解决问题
重新看待问题
public void AService{
@Resourece
private AMapper a;
@Resourece
private BMapper b;
void test(){
//访问master数据源
a.selectA();
//访问slave数据源
b.selectB();
//list方法是属于BaseMapper类的方法,没有被DS注解修饰,不织入切换数据源的代码当前线程是master数据源
b.list(new LambdaQueryWrapper().eq("id",1));
}
}
@DS("master")
public Interface AMapper{
void selectA();
}
@DS("slave")
public Interface BMapper extend BaseMapper<B>{
void selectB();
}
修改方案
- AService不直接调用BMapper.list(),改为调用BService,再在BService的方法中调用BMapper
- AService不直接调用BMapper.list(),讲BMapper.list()抽取成private方法并用DS("slave")修饰,再调用此方法。
总结
曾经给自己定下了学习时,阅读XXX、XXX源码的目标,但是现在看来这是一个非常愚蠢的想法。源码编译耗时,断点Debug耗时,去学习去了解也非常耗时,但是学习出来的效果确实非常的糟糕,容易事倍功半。只有带着问题,带着目的去看去学源码,才能有的放矢,做到事半功倍。
本次学习感触有三:
- 只会API只能说明在应用层会使用,有实际的使用经验,但是如果产生问题只能凭借经验和印象去还原能用的方案。
- 解决问题时,只有深入了解其根本原理,才能一刀见血解决问题,避免重复试错返工,耗时耗力。
- 学习的理解深刻,初次分享文章成就感满满,我将再接再厉!如果有错误,不吝赐教,望指正。