MyBatis-Plus组合dynamic-datasource-spring-boot-starter的多数据库(数据源)ORM框架在SpringBoot下完整JavaConfig及工作机制简析
本文通过JavaConfig的方式,在SpringBoot下,完成了在MyBatis-Plus框架下多数据源组件dynamic-datasource的手动配置,并通过配置详解分析其工作机制
背景信息
技术框架背景信息
- SpringBoot 2.5
- MyBatis-Plus 3.4.3.1
- dynamic-datasource-spring-boot-starter 3.4.1
- MySQL 5.7
- 连接池:HikariCP(SpringBoot集成版本)
业务背景
笔者所维护的一个项目使用了SpringBoot+MyBatisPlus框架,单库框架。由于项目的数据库连接参数放在了Nacos的配置中心,并通过@ConfigurationProperties注解构建了一个配置Bean的方式导入配置,所以数据库连接没有使用SpringBoot的yaml进行配置,而是使用了JavaConfig的配置方式。
由于业务需要,当前站点需要连接一个新的库。这样就涉及到了在MyBatis-Plus框架下的多数据源连接。
在各大技术论坛上,MyBatis-Plus下的多数据源方案主要有以下2种
经过谨慎选择,笔者决定使用dynamic-datasource的方案。
TIPS
在这里要澄清一下,并不是说使用了Nacos的配置中心后,就不能使用autoconfiguration功能了。事实上将yaml的配置从application.yaml迁移至配置中心后,同样可以参照原有配置格式进行配置。写这篇文章只是为了跟大家探究一下dynamic-datasource的工作本质和通过autoconfiguration进行的工作机制
dynamic-datasource
这是一款在DataSource层面的数据库动态切换框架。基础信息如下
配置及原理探究
名词解释
下文中所涉及到的一些名词统一在此做解释。
| 名词 | 解释 |
|---|---|
| demo.* | 【包名】代表着项目内部包路径,统一使用了demo.代替真实包名 |
| DatabaseConnectionProperties | 【配置类】通过@ConfigurationProperties从配置中获取的实体配置Bean,里面存储了数据库连接的账户信息 |
| DatabaseConstants | 【常量类】通过常量类的方式固定了数据库相关的常量信息 |
| DB_NAME_1 | 【常量值】DatabaseConstants下存储的数据库1的数据库名 |
| DB_NAME_2 | 【常量值】DatabaseConstants下存储的数据库2的数据库名 |
ORM框架配置原理解析
在Spring框架下,通过ORM框架(以Mybatis举例)访问数据库,通常需要构建如下几个Bean
- JDBC连接
- DataSource
- ORM框架(SqlSessionFactory)
- TransactionManager
而如上各个Bean的构建顺序及层级关系如下
dynamic-datasource的工作层级分析
我们从dynamic-datasource框架的官方文档中,可以追踪到配置类DynamicDataSourceProperties,同时,查找其调用,可追踪到自动配置类DynamicDataSourceAutoConfiguration,如下图:
根据DynamicDataSourceAutoConfiguration,我们可得知,dynamic-datasource构建了如下主要Bean
| Bean名称 | 用途 |
|---|---|
| dataSource | 实现了DataSource接口,实际是一个自定义的动态数据源实现DynamicRoutingDataSource,具有@ConditionalOnMissingBean注解性质 |
| dynamicDatasourceAnnotationAdvisor | 实现了Advisor接口,实质上是针对@DS注解的一个切面,用于切换数据源 |
| dynamicTransactionAdvisor | 实现了Advisor接口,事实上这个AOP实例实现了通过@DSTransactional注解在多数据源下的提交和回滚。具体分析可以看这一篇文章《DSTransactional实现源码分析》 |
其它Bean用于辅助上述三个Bean的构造,并不是重点。
所以,至此我们已经能够很清晰地了解到,dynamic-datasource实际是工作在了DataSource层,通过自定义的DataSource实现和AOP实现的配合,完成数据源切换。
Mybatis-Plus+dynamic-datasource框架下的配置构建
框架构建图
根据上一小节的分析,在Mybatis-Plus+dynamic-datasource组合下,数据访问框架的实际配置流程如下:
JavaConfig配置构建说明
这里我们虽然不使用典型的AutoConfiguration配置方式,即不在配置文件中写配置,但是我们依旧会利用AutoConfiguration框架,来辅助我们避免繁琐配置。具体方法是:
- 手动构建DynamicDataSourceProperties的Bean,并为其加上
@Primary注解,使得其可以被DynamicDataSourceAutoConfiguration使用,完成部分Bean的操作 - 参照
DynamicDataSourceAutoConfiguration中DataSource的构建方式,手动构建DynamicRoutingDataSource。(注意:DynamicDataSourceAutoConfiguration中DataSource有@ConditionalOnMissingBean注解性质,所以手动构建后,其内部的dataSource就不会再构建了) - 通过
DynamicRoutingDataSource内方法获取到的各个库的独立dataSource,构建TransactionManager
JavaConfig配置代码拆解
下面会根据上方配置层级图的方式,从底层开始逐层展示各个Bean的构建代码
包路径及引用
所有的包引用都在这
package demo.config;
import cn.hutool.db.dialect.DialectFactory;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.hikari.HikariCpConfig;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import demo.config.properties.DatabaseConnectionProperties;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
import static demo.constants.DatabaseConstants.*;
类及类变量
/**
* MySqlConfig
* 用于配置MySQL连接
*
* @author John Chen
* @since 2021/7/22
*/
@Configuration
@MapperScan("demo.*.mapper")
public class MySqlConfig {
/**
* 数据库连接参数配置
*/
private final DatabaseConnectionProperties properties;
public MySqlConfig(DatabaseConnectionProperties properties) {
this.properties = properties;
}
}
DynamicDataSourceProperties
这里构建时必须加上@Primary,否则Spring会报有多个Bean实例的错误
@Primary
@Bean
public DynamicDataSourceProperties dynamicDataSourceProperties() {
DynamicDataSourceProperties properties = new DynamicDataSourceProperties();
properties.setPrimary(DB_NAME_1);
properties.setStrict(true);
properties.setHealth(true);
properties.getDatasource().put(DB_NAME_2, db2DataSourceProperty());
properties.getDatasource().put(DB_NAME_1, db1DataSourceProperty());
return properties;
}
private DataSourceProperty db2DataSourceProperty() {
DataSourceProperty property = new DataSourceProperty();
DatabaseConnectionProperties.MySQLConfigEntity configEntity = properties.getDb2();
property.setUrl(configEntity.getUrl());
property.setUsername(configEntity.getUsername());
property.setPassword(configEntity.getPassword());
property.setDriverClassName(DialectFactory.DRIVER_MYSQL_V6);
//连接池名称配置,默认为HikariDataSource+数据库名称+Pool
property.setPoolName("HikariDataSourcePool_" + DB_NAME_2);
HikariCpConfig hikariConfig = new HikariCpConfig();
//region 固定连接池配置
hikariConfig.setMinIdle(1);
hikariConfig.setMaxPoolSize(15);
hikariConfig.setIdleTimeout(30000L);
hikariConfig.setMaxLifetime(1800000L);
hikariConfig.setConnectionTimeout(30000L);
hikariConfig.setConnectionTestQuery("select 1");
hikariConfig.setConnectionInitSql("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
property.setHikari(hikariConfig);
return property;
}
private DataSourceProperty db1DataSourceProperty() {
DataSourceProperty property = new DataSourceProperty();
DatabaseConnectionProperties.MySQLConfigEntity configEntity = properties.getDb1();
property.setUrl(configEntity.getUrl());
property.setUsername(configEntity.getUsername());
property.setPassword(configEntity.getPassword());
property.setDriverClassName(DialectFactory.DRIVER_MYSQL_V6);
//连接池名称配置,默认为HikariDataSource+数据库名称+Pool
property.setPoolName("HikariDataSourcePool_" + DB_NAME_1);
HikariCpConfig hikariConfig = new HikariCpConfig();
//region 固定连接池配置
hikariConfig.setMinIdle(1);
hikariConfig.setMaxPoolSize(15);
hikariConfig.setIdleTimeout(30000L);
hikariConfig.setMaxLifetime(1800000L);
hikariConfig.setConnectionTimeout(30000L);
hikariConfig.setConnectionTestQuery("select 1");
hikariConfig.setConnectionInitSql("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
property.setHikari(hikariConfig);
return property;
}
DynamicRoutingDataSource
这个DataSource实例会代替原配置类中的bean构建
/**
* 这里通过dynamic-datasource框架的自定义动态数据源构建了一个{@link DataSource}的Bean
* 注意,如果要在多数据源下试用统一提交和回滚事务,请试用注解{@link com.baomidou.dynamic.datasource.annotation.DSTransactional}
* 详情见文章《DSTransactional实现源码分析》
* <p>
* https://www.yinxiang.com/everhub/note/ac0175c8-35f5-4d66-8cd3-c662d7a16441
*
* @param properties 多数据源对应配置
* @return 返回DataSource
*/
@Bean
public DynamicRoutingDataSource dataSource(DynamicDataSourceProperties properties) {
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;
}
MyBatis-Plus构建(包含插件构建)
其实最开始我自己也不清楚加入了dynamic-datasource后,MyBatis-Plus应该如何构建。事实上由于2套框架工作在不同层,他们的配置完全不会互相影响。所以这里依旧是MyBatis-Plus的传统的JavaConfig构建方式。
/**
* 构建SqlSessionFactory
* <p>
* 注意,这里构建后会影响自动配置类{@link com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory(DataSource)}
* 的构建。后期如果增加插件,请务参照对照方法在内部增加注入配置
*/
@Bean
public SqlSessionFactory SqlSessionFactory(DataSource dataSource,GlobalConfig globalConfig, MybatisPlusInterceptor paginationInterceptor) throws Exception {
final MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/**/**Mapper.xml")
);
sqlSessionFactoryBean.setTypeAliasesPackage("demo.mapper.*");
sqlSessionFactoryBean.setTypeEnumsPackage("demo.*.enumutil");
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
sqlSessionFactoryBean.setConfiguration(defaultMybatisConfiguration());
Interceptor[] plugins = new Interceptor[]{paginationInterceptor};
sqlSessionFactoryBean.setPlugins(plugins);
return sqlSessionFactoryBean.getObject();
}
/**
* 默认的MybatisPlus配置
*
* @return MybatisPlus配置
*/
private MybatisConfiguration defaultMybatisConfiguration() {
MybatisConfiguration configuration = new MybatisConfiguration();
//打开自动驼峰命名转换
configuration.setMapUnderscoreToCamelCase(true);
//打开二级缓存
configuration.setCacheEnabled(true);
//打开延迟加载开关
configuration.setLazyLoadingEnabled(true);
//将积极加载改为消极按需加载
configuration.setAggressiveLazyLoading(false);
//是否允许单一语句返回多结果集(需要兼容驱动)。[默认值]
configuration.setMultipleResultSetsEnabled(true);
//使用列标签代替列名。不同的驱动在这方面会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果。[默认值]
configuration.setUseColumnLabel(true);
//使用MybatisPlus的枚举处理器,用于完成枚举处理
configuration.setDefaultEnumTypeHandler(MybatisEnumTypeHandler.class);
return configuration;
}
/**
* 创建全局配置
*
* @return 全局配置
*/
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
// 默认为自增
dbConfig.setIdType(IdType.AUTO);
globalConfig.setDbConfig(dbConfig);
return globalConfig;
}
/**
* 分页插件,自动识别数据库类型
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
TransactionManager
这里的TransactionManager要注意一下,由于这里的事务管理器是针对具体数据源进行构建的,所以后期在使用@Transactional注解时要注意:
- 注解中要标明对应的事务管理器实现
- 被注解的方法内,不要出现跨数据源调用。否则数据源切换会失效。详见文档说明
- 如果存在跨数据源调用的情况,请使用
@DSTransactional
@Bean
@Primary
public DataSourceTransactionManager db1TransactionManager(DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource.getDataSource(DB_NAME_1));
}
@Bean
public DataSourceTransactionManager db2TransactionManager( DynamicRoutingDataSource dataSource) {
return new DataSourceTransactionManager(dataSource.getDataSource(DB_NAME_2));
}
其它ORM框架下dynamic-datasource的使用
事实上,到这里我们已经发现,虽然MyBatis-Plus官方文档中推荐使用了dynamic-datasource这个组件做多数据源切换。但这个组件实际并没有和MyBatis-Plus有任何耦合。相反,该组件由于工作在DataSource层,可以工作在任何使用DataSource作为底层连接源的ORM框架下。具体配置可参照具体ORM框架配置说明和下方配置关系图
使用注意事项
- 本文仅做了
dynamic-datasource的构建分析,具体这个框架的使用请参考其技术文档和源码 - 根据一些技术文献的描述来看,
@DSTransactional是一个不完全的事务管理器。使用时务必先了解其特性 JavaConfig完成了更加复杂的场景下的实例配置。而AutoConfiguration让我们能够更加轻松地完成项目配置。二者本质上没有优劣之分。但我个人更喜欢JavaConfig,因为通过JavaConfig配置,你能够更加深入地了解构建对象的基础工作原理,对于技术也是个不错的提升机会。- 如发现本文不成熟之处,欢迎留言或私信提出你的宝贵建议~
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)