SpringBoot整合MybatisPlus多数据源
相信在很多使用MybatisPlus框架的小伙伴都会遇到多数据源的配置问题,并且官网也给出了推荐使用多数据源 (dynamic-datasource-spring-boot-starter) 组件来实现。由于最近项目也在使用这个组件来实现多数据源切换,因此想了解一下该组件是如何运行的,经过自己的调试,简单记录一下这个组件的实现,也以便日后组件如果出问题了或者某些地方需要开次开发时有个参考。
1 简单实现数据源切换
1.1 数据库demo
本例子使用的是同一个MYSQL服务,不同数据库来进行调试的,具体如图所示
建表语句如下:
CREATE TABLE `class_t` (`name` varchar(30) DEFAULT NULL,`number` varchar(30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `user_t` (`id` int(11) NOT NULL AUTO_INCREMENT,`user_name` varchar(30) DEFAULT NULL,`user_sex` varchar(30) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1.2 SpringBoot demo
1.2.1 添加依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.4.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version><scope>provided</scope></dependency>
1.2.2 配置YML文件
spring:datasource:type: com.zaxxer.hikari.HikariDataSourcehikari:minimum-idle: 5maximum-pool-size: 15idle-timeout: 30000max-lifetime: 1800000connection-timeout: 30000pool-name: OasisHikariCPconnection-test-query: SELECT 1dynamic:primary: db1 #默认主数据源datasource:db1: #配置主数据源url: jdbc:mysql://127.0.0.1:3306/test?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverdb2: #配置其他数据源url: jdbc:mysql://127.0.0.1:3306/test2?useSSL=true&requireSSL=false&serverTimezone=UTC&characterEncoding=UTF-8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
1.2.3 实体层
UserEntity.java
/*** @description: DB1中的实体* @date: 2021/7/13 13:38 <br>* @author: wuKeFan <br>* @version: 1.0 <br>*/
@TableName("user_t")
public class UserEntity {private long id;private String userName;private String userSex;public long getId() {return id;}public void setId(long id) {this.id = id;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName = userName;}public String getUserSex() {return userSex;}public void setUserSex(String userSex) {this.userSex = userSex;}
}
ClassEntity.java
/*** @description: DB2中的实体* @date: 2021/7/13 13:40 <br>* @author: wuKeFan <br>* @version: 1.0 <br>*/
@TableName("class_t")
public class ClassEntity {private String name;private String number;public String getName() {return name;}public void setName(String name) {this.name = name;}public String getNumber() {return number;}public void setNumber(String number) {this.number = number;}
}
1.2.4 mapper层
UserMapper.java(使用默认数据源)
/*** @description: UserMapper <br>* @date: 2021/7/13 13:41 <br>* @author: wuKeFan <br>* @version: 1.0 <br>*/
public interface UserMapper extends BaseMapper<UserEntity> {
}
ClassMapper.java(使用另外一个数据源)
/*** @description: ClassMapper <br>* @date: 2021/7/13 13:41 <br>* @author: wuKeFan <br>* @version: 1.0 <br>*/
@DS("db2") //使用另外一个数据源
public interface ClassMapper extends BaseMapper<ClassEntity> {
}
1.2.5 单元测试
结果已经是可以完美运行多数据源。
2 源码解析
在我们搞项目中,不仅要学会用这些组件,更重要的是 要知其所以然,知道他是如何实现的,其实原理也就是网上能搜到的基于切面的代理处理方式,但是其中有些内容还是值得去学习。
2.1 自动装配
首先我们从 dynamic-datasource组件的自动装配开始
接下来让我们来看一下 这个自动装配类,所装配的Bean
@Slf4j
@Configuration
//启动SpringBoot 自动装配 DynamicDataSourceProperties外部化配置
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
//声明装配加载顺序,在 DataSourceAutoConfiguration 之前加载
@AutoConfigureBefore(value = DataSourceAutoConfiguration.class, name = "com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure")
//当自动装配时,引入并自动装配下列三个自动装配类
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
//自动装配加载条件 当 spring.datasource.dynamic = true时 进行自动装配的加载,默认缺省为true
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {//注入外部化配置private final DynamicDataSourceProperties properties;private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;//构造函数注入public DynamicDataSourceAutoConfiguration(DynamicDataSourceProperties properties,ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {this.properties = properties;this.dataSourcePropertiesCustomizers = dataSourcePropertiesCustomizers.getIfAvailable();}//多数据源加载接口,默认的实现为从yml信息中加载所有数据源 @Beanpublic DynamicDataSourceProvider ymlDynamicDataSourceProvider() {return new YmlDynamicDataSourceProvider(properties.getDatasource());}//实现DataSource JAVA JNDI 后期Spring 容器中 所有的数据库连接都从该实现Bean 中获取@Bean@ConditionalOnMissingBeanpublic DataSource dataSource() {DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();dataSource.setPrimary(properties.getPrimary());dataSource.setStrict(properties.getStrict());dataSource.setStrategy(properties.getStrategy());dataSource.setP6spy(properties.getP6spy());dataSource.setSeata(properties.getSeata());return dataSource;}//设置动态数据源转换切换配置器@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)@Beanpublic Advisor 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)@Beanpublic Advisor dynamicTransactionAdvisor() {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");return new DefaultPointcutAdvisor(pointcut, new DynamicLocalTransactionAdvisor());}//DynamicDataSourceAnnotationInterceptor 切面配置器中所需要的执行链,//主要用来确定使用哪个数据源@Bean@ConditionalOnMissingBeanpublic DsProcessor dsProcessor(BeanFactory beanFactory) {DsHeaderProcessor headerProcessor = new DsHeaderProcessor();DsSessionProcessor sessionProcessor = new DsSessionProcessor();DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));headerProcessor.setNextProcessor(sessionProcessor);sessionProcessor.setNextProcessor(spelExpressionProcessor);return headerProcessor;}//Bean注入后所执行的方法,本Demo中目前暂无使用@Overridepublic void afterPropertiesSet() {if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) {for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) {customizer.customize(properties);}}}}
大体上的自动装配已经介绍完了,接下来我们逐个将重要的代码段或者类来进行解释
2.2 DynamicDataSourceCreatorAutoConfiguration类分析
这个类主要是进行数据源加载的 主要代码如下
@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {//描述Bean的 注入顺序public static final int JNDI_ORDER = 1000;public static final int DRUID_ORDER = 2000;public static final int HIKARI_ORDER = 3000;public static final int BEECP_ORDER = 4000;public static final int DBCP2_ORDER = 5000;public static final int DEFAULT_ORDER = 6000;private final DynamicDataSourceProperties properties;//默认的数据源创造器@Primary@Bean@ConditionalOnMissingBeanpublic DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSourceCreators) {DefaultDataSourceCreator defaultDataSourceCreator = new DefaultDataSourceCreator();defaultDataSourceCreator.setProperties(properties);defaultDataSourceCreator.setCreators(dataSourceCreators);return defaultDataSourceCreator;}//省略部分代码/*** 存在Hikari数据源时, 加入创建器*/@ConditionalOnClass(HikariDataSource.class)@Configurationpublic class HikariDataSourceCreatorConfiguration {@Bean@Order(HIKARI_ORDER)@ConditionalOnMissingBeanpublic HikariDataSourceCreator hikariDataSourceCreator() {return new HikariDataSourceCreator(properties.getHikari());}}//省略部分代码}
当Spring 容器注入 DefaultDataSourceCreator 实例后 ,接下来就被 DynamicDataSourceProvider 这个类所使用。
2.3 DynamicDataSourceProvider 分析
@Slf4j
@AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {/*** 所有数据源*/private final Map<String, DataSourceProperty> dataSourcePropertiesMap;//通过构造函数注入所有的 数据源 然后调用该父类方法创建数据源集合@Overridepublic Map<String, DataSource> loadDataSources() {return createDataSourceMap(dataSourcePropertiesMap);}
}
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {//从Spring 容器中获取注入好的 DefaultDataSourceCreator @Autowiredprivate 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);dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));}return dataSourceMap;}
}
2.4 DynamicDataSourceAnnotationAdvisor 分析
这个其实就是Spring AOP的切面配置器 主要代码如下
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();}@Overridepublic Pointcut getPointcut() {return this.pointcut;}@Overridepublic Advice getAdvice() {return this.advice;}//省略部分代码//当有类或者方法中有 DS.class 注解时 进行 切面增强private Pointcut buildPointcut() {Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);Pointcut mpc = new AnnotationMethodPoint(DS.class);return new ComposablePointcut(cpc).union(mpc);}//省略部分代码
}
2.5 DynamicDataSourceAnnotationInterceptor 分析
该类为切面增强,即当上面的DynamicDataSourceAnnotationAdvisor 拦截到类或者方法中有 DS.class 注解时 ,调用该增强类进行处理
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;}//AOP拦截后进行 切面增强方法@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {//选择数据源String dsKey = determineDatasourceKey(invocation);//使用基于ThreadLocal的实现切换数据源DynamicDataSourceContextHolder.push(dsKey);try {return invocation.proceed();} finally {DynamicDataSourceContextHolder.poll();}}//通过调用 DsProcessor 来链式调用进行 数据源的确认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;}
}
2.6 DefaultPointcutAdvisor 分析
该切面增强为事务增强,设置此增强类后,不能与Spring 源事务或者 @Transactional 注解共用。
@Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {if (!StringUtils.isEmpty(TransactionContext.getXID())) {return methodInvocation.proceed();}boolean state = true;Object o;String xid = UUID.randomUUID().toString();TransactionContext.bind(xid);try {o = methodInvocation.proceed();} catch (Exception e) {state = false;throw e;} finally {ConnectionFactory.notify(state);TransactionContext.remove();}return o;}
}
2.7 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") {@Overrideprotected 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 String push(String ds) {String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;LOOKUP_KEY_HOLDER.get().push(dataSourceStr);return dataSourceStr;}/*** 清空当前线程数据源* <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();}
}
大致核心的代码已经介绍完了,接下来我们逐步debugger,摸清其执行流程。
2.8 数据源切换执行流程
现在当我们执行上面的SpringBoot demo中的 调用注解 @DS("db2") 的 Mapper 查询数据库时,他的顺序如下(只给出涉及到该组件的相关类)
ClassMapper#selectList() : 执行Mybatis查询操作
DynamicDataSourceAnnotationInterceptor#invoke() : Spring AOP 拦截到带有 @DS("db2") 并执行代理增强操作
DataSourceClassResolver#findDSKey() : 查找有注解@DS() 的 类或方法,获取对应的数据源Key 值 也就是 db2。
DynamicDataSourceContextHolder#push() : 设置当前线程数据源
DynamicRoutingDataSource#getConnection(): 调用父类方法获取数据库连接 这里两种处理方式 如下所示
public Connection getConnection() throws SQLException {String xid = TransactionContext.getXID();//无事务时 即当前操作为 查询if (StringUtils.isEmpty(xid)) {return determineDataSource().getConnection();} else {//有事物时 ,先从 之前DynamicDataSourceContextHolder 中获取数据源 先进先出原则String ds = DynamicDataSourceContextHolder.peek();ds = StringUtils.isEmpty(ds) ? "default" : ds;ConnectionProxy connection = ConnectionFactory.getConnection(ds);//创建数据源return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;}}
DynamicRoutingDataSource#getDataSource 设置数据源
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 CannotFindDataSourceException("dynamic-datasource could not find a datasource named" + ds);}return determinePrimaryDataSource();}
执行剩余的数据库操作至结束。
3 小结
大体上写的略微混乱,但是只要我们知道其自动装配时 ,实例化了哪些Bean,并且知道这些Bean 是干什么的 ,合适调用的,根据执行流程逐步Debugger调试,就可以明白dynamic-datasource组件是如何进行数据源切换的,在流程中我认为比较经典也是比较核心的地方已经标注出源码。我们可以借鉴 DynamicDataSourceContextHolder 这个公共类的思想,扩展和优化我们现有的项目中某些跨资源调用的问题。
SpringBoot整合MybatisPlus多数据源相关推荐
- 超详细教程:SpringBoot整合MybatisPlus
本文分享自华为云社区<SpringBoot整合MybatisPlus[超详细]>,原文作者:牛哄哄的柯南. 创建个SpringBoot项目 选生所需的依赖:== 我把application ...
- SpringBoot整合Mybatis-Plus,代码生成器Generator以及Swagger(附源码、图文学习、Postman、ApiPost第三方工具的使用)
目录 一.SpringBoot整合Mybatis-plus 1.引入依赖 2.创建数据库 3.整合代码生成器Generator 二.什么是Swagger2,有什么作用? 三.SpringBoot整合S ...
- 10-Java框架-SpringBoot整合MyBatis-Plus
一.MyBatis-Plus介绍 官网 https://baomidou.com/ MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变 ...
- SpringBoot整合MyBatis-Plus分页查询
在整合mybatis-plus时可以先参考官网:快速开始 一.引入依赖 <dependency><groupId>org.springframework.boot</gr ...
- SpringBoot整合Mybatis-plus实现增删查改
今天给大家分享一下SpringBoot整合Mybatis-plus的增删查改案例. pom.xml <?xml version="1.0" encoding="UT ...
- SpringBoot整合阿里Druid数据源及Spring-Data-Jpa
SpringBoot整合阿里Druid数据源及Spring-Data-Jpa https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=224 ...
- 实践丨SpringBoot整合Mybatis-Plus项目存在Mapper时报错
本文分享自华为云社区<SpringBoot整合MybatisPlus项目存在Mapper时运行报错的问题分析与修复>,作者:攻城狮Chova . 异常信息 在SpringBoot运行测试M ...
- SpringBoot整合Mybatis-Plus
这篇文章介绍一个SpringBoot整合Mybatis-Plus,提供一个小的Demo供大家参考. 已经很久没有写文章了,最近家里有点事刚刚处理完,顺便也趁机休息了一段时间.刚回到公司看了一下码云,发 ...
- Springboot整合JPA多数据源(Oracle+Mysql)
Springboot整合JPA多数据源 1. Maven 2. 基本配置 2.1 DataSource 3. 多数据源配置 3.1 JpaConfigOracle 3.2 JpaConfigMysql ...
最新文章
- c++错误之map的find()返回值类型
- 字符串之从规定的字符串中根据下标得到字符
- 如何提高写前端的效率?干货,快进!
- python求近似值_python 已知一个字符,在一个list中找出近似值或相似值实现模糊匹配...
- 使用React,TypeScript和Socket.io构建聊天应用
- python改变turtle画笔方向的函数_哪个选项不能改变turtle画笔的运行方向?
- 【数据结构和算法笔记】:图的深度优先搜索(DFS)
- vue2.x 给一个对象里添加一个没有的属性
- asBroadcastStream
- C++(数据结构与算法):16---特殊矩阵的实现(对角矩阵、三对角矩阵、下三角矩阵、上三角矩阵、对称矩阵)
- Java——JButton按钮
- 氚云无代码搭建学习记录
- oracle序列建同义词,oracle序列创建同义词
- 安卓平板usb变显示器_三星DeX—安卓生产力之巅
- Emlog大表哥资源网模板
- 网站的robots 文件写法 附自动生成工具网址
- 统一社会信用代码 php验证
- 环形电流计算公式_环形电流的磁场分布 怎么计算
- ABAP 负数符号在前面显示/如何将负数改为正数
- unity中awak,start,update之间的关系(转)
热门文章
- 怎么解决mysql中文显示_怎样解决关于MySQL显示中文的问题?
- 计算机毕业设计Java健身房管理系统演示录像2021(源码+系统+mysql数据库+lw文档)
- 前端<a>标签超链接,触发点击事件后弹出about:blank#blocked页
- 得墨忒耳定律(Law of Demeter)
- 网络服务的两种处理模型(Nginx为什么比Apache好)
- MySQL慢SQL调优
- 2013 后苹果 MAC 安装 WINDOWS 教程
- 雅点动态水波ps扩展面板破解版
- C++笔记:有关free函数
- CryptoPunks这样像素艺术为什么能依然流行?