一、简介
dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
由MyBatisPlus的团队baomidou开发,其支持 Jdk 1.7+, SpringBoot 1.5.x 2.x.x 3.x.x。
JPA用户不建议使用,JPA自带事务,无法连续切库。
二、特性
- 支持数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息加密(可自定义) ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 基于seata的分布式事务方案 。
- 提供 本地多数据源事务方案。
三、约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
- 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
四、基础使用
- 引入dynamic-datasource-spring-boot-starter。
spring-boot 1.5.x 或 2.x.x
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
spring-boot 3
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${version}</version>
</dependency>
- 配置数据源。
yaml
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
yaml
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
3.使用 @DS 切换数据源。
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
| 注解 | 结果 |
|---|---|
| 没有@DS | 默认数据源 |
| @DS("dsName") | dsName可以为组名也可以为具体某个库的名称 |
java
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
- @DS注解的使用位置
其实没有一定的要求,看实际情况:
- DS注解是基于AOP的原理实现的,aop的常见失效场景应清楚。 比如内部调用失效,shiro代理失效等。
- 通常建议DS放在serviceImpl的方法上,如事务注解一样。
- 可以注解在Controller的方法上或类上,但不建议,原因是controller的操作常常并不涉及数据库。
- 注解在mapper上,如果你某个Mapper对应的表只在确定的一个库,也是可以的。 但是建议只注解在Mapper的类上。
- 支持继承抽象类上的DS注解
- 支持继承接口上的DS(3.4.1开始支持),但是一个类能实现多个接口,如果多个接口都有DS会有问题。
五、连接池配置
每个数据源都有一个type来指定连接池。
每个数据源甚至可以使用不同的连接池,如无特殊需要并不建议。
type 不是必填字段 ,在没有设置type的时候系统会自动按以下顺序查找并使用连接池
Druid>HikariCp>BeeCp>DBCP2>Spring Basic
yaml
spring:
datasource:
dynamic:
primary: db1
datasource:
db1:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource #使用Hikaricp
db2:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource #使用Druid
db3:
url: jdbc:mysql://xx.xx.xx.xx:3308/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: cn.beecp.BeeDataSource #使用beecp
集成连接池(以Druid为例)
- 项目引入 druid-spring-boot-starter 依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
- 排除原生Druid的快速配置类。
java
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
某些springBoot的版本上面可能无法排除可用以下方式排除。
yaml
spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
- 参数配置
如果参数都未配置,则保持原组件默认值。
如果配置了全局参数,则每一个数据源都会继承对应参数。
每一个数据源可以单独设置参数覆盖全局参数。
yaml
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
dynamic:
druid: #以下是支持的全局默认值
initial-size:
max-active:
filters: stat # 注意这个值和druid原生不一致,默认启动了stat。 如果确定什么filter都不需要 这里填 ""
...等等基本都支持
wall:
none-base-statement-allow:
...等等
stat:
merge-sql:
log-slow-sql:
slow-sql-millis:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
druid: # 以下是独立参数,每个库可以重新设置
initial-size: 20
validation-query: select 1 FROM DUAL #比如oracle就需要重新设置这个
public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter,推荐使用本项目自带的加密方法。
# ......
# 生成 publickey 和密码,推荐使用本项目自带的加密方法。
# java -cp druid-1.1.10.jar com.alibaba.druid.filter.config.ConfigTools youpassword
六、三方集成
- 集成MybatisPlus
只要引入了mybatisPlus相关jar包,项目自动集成,兼容mybatisPlus 2.x和3.x的版本
- 分页配置
java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//如果是不同类型的库,请不要指定DbType,其会自动判断。
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
- 注意事项
mp内置的ServiceImpl在新增,更改,删除等一些方法上自带事物导致不能切换数据源。
解决办法:
(1)复制ServiceImpl出来为自己的MyServiceImpl,并去掉所有事务注解。
(2)创建一个新方法,内部调用Mapper,并在此方法上添加DS注解。
- 集成P6spy
yaml
spring:
datasource:
dynamic:
p6spy: true # 默认false,建议线上关闭。
datasource:
product:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
p6spy: false # 如果这个库不需要可单独关闭。
order:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
七、动态添加移除数据源
主要在多租户场景中,一个新的租户进来需要动态的添加一个数据源到库中,使得系统不用重启即可切换数据源。
java
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.*;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.samples.ds.dto.DataSourceDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.sql.DataSource;
import java.util.Set;
@RestController
@RequestMapping("/datasources")
@Api(tags = "添加删除数据源")
public class DataSourceController {
@Autowired
private DataSource dataSource;
// private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用,强烈推荐sb2用户至少升级到3.5.2版本
@Autowired
private DefaultDataSourceCreator dataSourceCreator;
//如果是用4.x以上版本,因为要和spring解绑,重构了一些东西,比如缺少了懒启动和启动初始化数据库。不太建议用以下独立的创建器,只建议用上面的DefaultDataSourceCreator
@Autowired
private BasicDataSourceCreator basicDataSourceCreator;
@Autowired
private JndiDataSourceCreator jndiDataSourceCreator;
@Autowired
private DruidDataSourceCreator druidDataSourceCreator;
@Autowired
private HikariDataSourceCreator hikariDataSourceCreator;
@Autowired
private BeeCpDataSourceCreator beeCpDataSourceCreator;
@Autowired
private Dbcp2DataSourceCreator dbcp2DataSourceCreator;
@GetMapping
@ApiOperation("获取当前所有数据源")
public Set<String> now() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
return ds.getDataSources().keySet();
}
//通用数据源会根据maven中配置的连接池根据顺序依次选择。
//默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
@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.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addBasic(强烈不推荐,除了用了马上移除)")
@ApiOperation(value = "添加基础数据源", notes = "调用Springboot内置方法创建数据源,兼容1,2")
public Set<String> addBasic(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = basicDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addJndi")
@ApiOperation("添加JNDI数据源")
public Set<String> addJndi(String pollName, String jndiName) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = jndiDataSourceCreator.createDataSource(jndiName);
ds.addDataSource(poolName, dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addDruid")
@ApiOperation("基础Druid数据源")
public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addHikariCP")
@ApiOperation("基础HikariCP数据源")
public Set<String> addHikariCP(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addBeeCp")
@ApiOperation("基础BeeCp数据源")
public Set<String> addBeeCp(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = beeCpDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@PostMapping("/addDbcp")
@ApiOperation("基础Dbcp数据源")
public Set<String> addDbcp(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = dbcp2DataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPoolName(), dataSource);
return ds.getDataSources().keySet();
}
@DeleteMapping
@ApiOperation("删除数据源")
public String remove(String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.removeDataSource(name);
return "删除成功";
}
}
八、动态解析数据源
默认有三个职责链来处理动态参数解析器 header->session->spel
所有以 # 开头的参数都会从参数中获取数据源。
java
@DS("#session.tenantName")//从session获取
public List selectSpelBySession() {
return userMapper.selectUsers();
}
@DS("#header.tenantName")//从header获取
public List selectSpelByHeader() {
return userMapper.selectUsers();
}
@DS("#tenantName")//使用spel从参数获取
public List selectSpelByKey(String tenantName) {
return userMapper.selectUsers();
}
@DS("#user.tenantName")//使用spel从复杂参数获取
public List selecSpelByTenant(User user) {
return userMapper.selectUsers();
}
- 自定义参数解析
参考header解析器,继承DsProcessor,如果matches返回true则匹配成功,调用doDetermineDatasource返回匹配到的数据源,否则跳到下一个解析器.
java
public class DsHeaderProcessor extends DsProcessor {
//前缀
private static final String HEADER_PREFIX = "#header";
@Override
public boolean matches(String key) {
return key.startsWith(HEADER_PREFIX);
}
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取请求头中key为"xxx"的值,完整表达式是"#header.xxx"
return request.getHeader(key.substring(HEADER_PREFIX.length()+1));
}
}
重写完后重新注入一个根据自己解析顺序的解析处理器
java
@Configuration
public class MyDynamicDataSourceConfig{
@Bean
public DsProcessor dsProcessor() {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
}
注意:如果在Mapper接口下面的方法使用
java
public interface UserMapper{
// 前缀可以是p0,a0
@DS("#p0.tenantName")
public List selecSpelByTenant(User user);
}
spel中只能使用a或p带下标指定参数, 如 a0, p0, a1, p1,...
原因是编译器的默认配置没有将接口参数的元数据写入字节码文件中.
如果要使用参数名须在maven中配置编译接口参数信息写入字节码,并在编译带上-parameters
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- 想启用 <parameters>true</parameters> 的maven编译最低版本为:3.6.2 -->
<version>3.6.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<parameters>true</parameters>
</configuration>
</plugin>
idea java编译配置: -parameters
九、配置加密
在一些项目中,有对数据库关键字段加密的需求。
本项目也支持支持url , username, password 的加密。
使用的RAS加密,私钥加密,公钥解密。
- 私钥加密,公钥解密
java
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
public class Demo {
public static void main(String[] args) throws Exception {
String password = "123456";
//使用默认的密钥对加解密(不推荐)
String encodePassword = CryptoUtils.encrypt(password);
System.out.println(encodePassword);
//使用自定义密钥对加解密(推荐)
String[] arr = CryptoUtils.genKeyPair(512);
//私钥用于加密,请自行保存,不要保存在项目代码或配置文件里
System.out.println("privateKey: " + arr[0]);
//公钥用于解密,需配置到yml中,生产环境配置可交给运维在启动时传入,避免暴露给开发人员
System.out.println("publicKey: " + arr[1]);
//分别对以下值加密得到密文,然后配置到配置文件中: ENC(密文)
System.out.println("url: " + CryptoUtils.encrypt(arr[0], "jdbc:mysql://127.0.0.1:3306/order"));
System.out.println("username: " + CryptoUtils.encrypt(arr[0], "root"));
System.out.println("password: " + CryptoUtils.encrypt(arr[0], "123456"));
}
}
- 配置加密yml
ENC(xxx)` 中包裹的xxx即为使用加密方法后生成的字符串。
yaml
spring:
datasource:
dynamic:
public-key: #有默认值,强烈建议更换
datasource:
master:
url: ENC(xxxxx)
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
public-key: #每个数据源可以独立设置,没有就继承上面的。
- 自定义解密
如果想使用自己的方式加密,解密。从3.5.0版本开始,扩展了一个event。用户自行实现注入即可。
java
public interface DataSourceInitEvent {
/**
* 连接池创建前执行(可用于参数解密)
*
* @param dataSourceProperty 数据源基础信息
*/
void beforeCreate(DataSourceProperty dataSourceProperty);
/**
* 连接池创建后执行
*
* @param dataSource 连接池
*/
void afterCreate(DataSource dataSource);
}
默认的实现如下,可参考
java
package com.baomidou.dynamic.datasource.event;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncDataSourceInitEvent implements DataSourceInitEvent {
private static final Logger log = LoggerFactory.getLogger(EncDataSourceInitEvent.class);
private static final Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$");
public void beforeCreate(DataSourceProperty dataSourceProperty) {
String publicKey = dataSourceProperty.getPublicKey();
if (DsStrUtils.hasText(publicKey)) {
dataSourceProperty.setUrl(this.decrypt(publicKey, dataSourceProperty.getUrl()));
dataSourceProperty.setUsername(this.decrypt(publicKey, dataSourceProperty.getUsername()));
dataSourceProperty.setPassword(this.decrypt(publicKey, dataSourceProperty.getPassword()));
}
}
public void afterCreate(DataSource dataSource) {
}
private String decrypt(String publicKey, String cipherText) {
if (DsStrUtils.hasText(cipherText)) {
Matcher matcher = ENC_PATTERN.matcher(cipherText);
if (matcher.find()) {
try {
return CryptoUtils.decrypt(publicKey, matcher.group(1));
} catch (Exception e) {
log.error("DynamicDataSourceProperties.decrypt error ", e);
}
}
}
return cipherText;
}
}
十、启动初始化执行脚本
3.5.0 之前版本
yaml
spring:
datasource:
dynamic:
primary: order
datasource:
order:
# 基础配置省略...
schema: db/order/schema.sql # 配置则生效,自动初始化表结构
data: db/order/data.sql # 配置则生效,自动初始化数据
continue-on-error: true # 默认true,初始化失败是否继续
separator: ";" # sql默认分号分隔符,一般无需更改
product:
schema: classpath*:db/product/schema.sql
data: classpath*:db/product/data.sql
user:
schema: classpath*:db/user/schema/**/*.sql
data: classpath*:db/user/data/**/*.sql
3.5.0(含) 之后版本 (层级多了一层init,保持和新版springboot一致)
yaml
spring:
datasource:
dynamic:
primary: order
datasource:
order:
# 基础配置省略...
init:
schema: db/order/schema.sql # 配置则生效,自动初始化表结构
data: db/order/data.sql # 配置则生效,自动初始化数据
continue-on-error: true # 默认true,初始化失败是否继续
separator: ";" # sql默认分号分隔符,一般无需更改
十一、无数据源启动
场景:部分系统可能启动的时候完全不需要数据源,全靠启动后动态添加。
- 配置方式
即基本不需要配置,可能根据需要配置一些类似Druid和HikariCp全局参数。
yaml
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
一般无数据源启动需要结合【动态添加移除数据源】在启动后添加
十二、手动切换数据源
java
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
DynamicDataSourceContextHolder.push("slave");//手动切换
return jdbcTemplate.queryForList("select * from user");
}
}
需要注意的是手动切换的数据源,最好自己在合适的位置 调用DynamicDataSourceContextHolder.clear()清空当前线程的数据源信息
java
@Slf4j
public class DynamicDatasourceClearInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//入口处清空看个人,有的新人在异步里切了数据源但是忘记清除了,下一个请求遇到这个线程就会带进来。
//DynamicDataSourceContextHolder.clear();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
DynamicDataSourceContextHolder.clear();
}
}
十三、自定义注解
如果你只有几个确定的库,可以尝试自定义注解替换掉@DS。
建议从3.2.1版本开始使用自定义注解,另外组件自带了@Master和@Slave注解。
- 需要自己定义一个注解并继承自DS。
java
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("product")
public @interface DSProduct {
}
- 注解在需要切换数据源的方法上或类上。
java
@Service
@DSProduct
public class ProductServiceImpl implements ProductService {
@DSProduct
public List selectAll() {
return jdbcTemplate.queryForList("select * from products");
}
}
十四、自定义数据源来源
数据源来源的默认实现是YmlDynamicDataSourceProvider,其从yaml或properties中读取信息并解析出所有数据源信息。
java
public interface DynamicDataSourceProvider {
/**
* 加载所有数据源
*
* @return 所有数据源,key为数据源名称
*/
Map<String, DataSource> loadDataSources();
}
自定义来源,可以参考 AbstractJdbcDataSourceProvider (仅供参考)来实现从JDBC数据库中获取数据库连接信息。
java
@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {
return new AbstractJdbcDataSourceProvider("org.h2.Driver", "jdbc:h2:mem:test", "sa", "") {
@Override
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
ResultSet rs = statement.executeQuery("select * from DB");
while (rs.next()) {
String name = rs.getString("name");
String username = rs.getString("username");
String password = rs.getString("password");
String url = rs.getString("url");
String driver = rs.getString("driver");
DataSourceProperty property = new DataSourceProperty();
property.setUsername(username);
property.setPassword(password);
property.setUrl(url);
property.setDriverClassName(driver);
map.put(name, property);
}
return map;
}
};
}
PS: 从3.4.0开始,可以注入多个DynamicDataSourceProvider的Bean以实现同时从多个不同来源加载数据源,注意同名会被覆盖
十五、自定义负载均衡策略
如下图slave组下有三个数据源,当用户使用slave切换数据源时会使用负载均衡算法。
系统自带了两个负载均衡算法
- LoadBalanceDynamicDataSourceStrategy 轮询,是默认的。
- RandomDynamicDataSourceStrategy 随机的
yaml
spring:
datasource:
dynamic:
datasource:
master:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
schema: db/schema.sql
slave_1:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
slave_2:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
slave_3:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy
如果默认的两个都不能满足要求,可以参考源码自定义。 暂时只能全局更改
java
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;
public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
public RandomDynamicDataSourceStrategy() {
}
public DataSource determineDataSource(List<DataSource> dataSources) {
return (DataSource)dataSources.get(ThreadLocalRandom.current().nextInt(dataSources.size()));
}
}
十六、动态路由数据源(无注解)
- 本插件多数据源实现的核心
java
public class DynamicRoutingDataSource {
@Override
public DataSource determineDataSource() {
//这里是核心,是从ThreadLocal中获取当前数据源。
String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}
- 所以我们就可以根据需求,选择合适的时机调用DynamicDataSourceContextHolder.push("对应数据源")。
- 默认的@DS注解来切换数据源是根据spring AOP的特性,在方法开启前设置数据源KEY,方法执行完成后移除对应数据源KEY。
- 在intercepror中切换(也可以在filter、aop中,可参考切换逻辑)
java
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class DynamicDatasourceInterceptor implements HandlerInterceptor {
/**
* 在请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String requestURI = request.getRequestURI();
log.info("经过多数据源Interceptor,当前路径是{}", requestURI);
// String headerDs = request.getHeader("ds");
// Object sessionDs = request.getSession().getAttribute("ds");
String s = requestURI.replaceFirst("/interceptor/", "");
String dsKey = "master";
if (s.startsWith("a")) {
dsKey = "db1";
} else if (s.startsWith("b")) {
dsKey = "db2";
} else {
}
DynamicDataSourceContextHolder.push(dsKey);
return true;
}
/**
* 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
}
/**
* 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
DynamicDataSourceContextHolder.clear();
}
}
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyWebAutoConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DynamicDatasourceInterceptor()).addPathPatterns("/interceptor/**");
}
}
注意 : 此处是选择一个数据源,不会创建数据源,数据源需要在启动时全部创建好或者动态添加。
可以参考【自定义数据源来源】或者参考【动态添加移除数据源】
也可以在启动时手动查询租户连接信息创建对应租户数据源,参考代码如下:
java
import com.saasbase.entity.Tenant;
import com.saasbase.mapper.TenantMapper;
import jakarta.annotation.PostConstruct;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
@Slf4j
@Component
@AllArgsConstructor
public class DataSourceLoader {
private final TenantMapper tenantMapper; // 查库
private final DynamicDataSourceManager dsManager;
@PostConstruct
public void init() {
//查询所有租户的连接信息
List<Tenant> tenantList = tenantMapper.selectList(null);
for (Tenant tenant : tenantList) {
//数据源添加
addTenantDataSource(
tenant.getId(),
tenant.getDbName(),
//todo 对user和password进行加解密,提高安全性
tenant.getDbUsername(),
tenant.getDbPassword(),
tenant.getDataSet() //使用默认前缀
);
}
}
}
java
public void addTenantDataSource(Long tenantId, String dbName, String dbUser, String dbPass, String urlPrefix) {
DataSourceProperty property = new DataSourceProperty();
String dsKey = "tenant_" + tenantId;
property.setPoolName(dsKey);
// 使用 @Value 注入的属性
property.setDriverClassName(tenantDriverClassName);
property.setUrl(urlPrefix + "/" + dbName + tenantUrlSuffix);
property.setUsername(dbUser);
property.setPassword(dbPass);
// 应用 Hikari 连接池配置
property.setHikari(hikariCpConfig);
DataSource newDs = dataSourceCreator.createDataSource(property);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.addDataSource(dsKey, newDs);
}
十七、部分源码(version 4.3.1)
- DynamicRoutingDataSource (动态数据源管理)
java
package com.baomidou.dynamic.datasource;
import com.baomidou.dynamic.datasource.destroyer.DataSourceDestroyer;
import com.baomidou.dynamic.datasource.destroyer.DefaultDataSourceDestroyer;
import com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.p6spy.engine.spy.P6DataSource;
import io.seata.rm.datasource.DataSourceProxy;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
private static final Logger log = LoggerFactory.getLogger(DynamicRoutingDataSource.class);
private static final String UNDERLINE = "_";
private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap();
private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap();
private final List<DynamicDataSourceProvider> providers;
private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
private String primary = "master";
private Boolean strict = false;
private Boolean p6spy = false;
private Boolean seata = false;
private Boolean graceDestroy = false;
public DynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {
this.providers = providers;
}
protected String getPrimary() {
return this.primary;
}
public DataSource determineDataSource() {
String dsKey = DynamicDataSourceContextHolder.peek();
return this.getDataSource(dsKey);
}
private DataSource determinePrimaryDataSource() {
log.debug("dynamic-datasource switch to the primary datasource");
DataSource dataSource = (DataSource)this.dataSourceMap.get(this.primary);
if (dataSource != null) {
return dataSource;
} else {
GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(this.primary);
if (groupDataSource != null) {
return groupDataSource.determineDataSource();
} else {
throw new CannotFindDataSourceException("dynamic-datasource can not find primary datasource");
}
}
}
public Map<String, DataSource> getDataSources() {
return this.dataSourceMap;
}
public Map<String, GroupDataSource> getGroupDataSources() {
return this.groupDataSources;
}
public DataSource getDataSource(String ds) {
if (DsStrUtils.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 ((GroupDataSource)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 CannotFindDataSourceException("dynamic-datasource could not find a datasource named " + ds);
} else {
return this.determinePrimaryDataSource();
}
}
public synchronized void addDataSource(String ds, DataSource dataSource) {
DataSource oldDataSource = (DataSource)this.dataSourceMap.put(ds, dataSource);
this.addGroupDataSource(ds, dataSource);
if (oldDataSource != null) {
this.closeDataSource(ds, oldDataSource, this.graceDestroy);
}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
}
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains("_")) {
String group = ds.split("_")[0];
GroupDataSource groupDataSource = (GroupDataSource)this.groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, (DynamicDataSourceStrategy)this.strategy.getDeclaredConstructor().newInstance());
this.groupDataSources.put(group, groupDataSource);
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
public synchronized void removeDataSource(String ds) {
if (!DsStrUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
} else if (this.primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
} else {
if (this.dataSourceMap.containsKey(ds)) {
DataSource dataSource = (DataSource)this.dataSourceMap.remove(ds);
if (ds.contains("_")) {
String group = ds.split("_")[0];
if (this.groupDataSources.containsKey(group)) {
DataSource oldDataSource = ((GroupDataSource)this.groupDataSources.get(group)).removeDatasource(ds);
if (oldDataSource == null) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
this.closeDataSource(ds, dataSource, this.graceDestroy);
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
}
public void destroy() {
log.info("dynamic-datasource start closing ....");
for(Map.Entry<String, DataSource> item : this.dataSourceMap.entrySet()) {
this.closeDataSource((String)item.getKey(), (DataSource)item.getValue(), false);
}
log.info("dynamic-datasource all closed success,bye");
}
public void afterPropertiesSet() {
this.checkEnv();
Map<String, DataSource> dataSources = new HashMap(16);
for(DynamicDataSourceProvider provider : this.providers) {
Map<String, DataSource> dsMap = provider.loadDataSources();
if (dsMap != null) {
dataSources.putAll(dsMap);
}
}
for(Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
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)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), this.primary);
} else {
log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
}
}
private void checkEnv() {
if (this.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 (this.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);
}
}
}
private void closeDataSource(String ds, DataSource dataSource, boolean graceDestroy) {
try {
DataSource realDataSource = null;
if (dataSource instanceof ItemDataSource) {
realDataSource = ((ItemDataSource)dataSource).getRealDataSource();
} else {
if (this.seata && dataSource instanceof DataSourceProxy) {
DataSourceProxy dataSourceProxy = (DataSourceProxy)dataSource;
realDataSource = dataSourceProxy.getTargetDataSource();
}
if (this.p6spy && dataSource instanceof P6DataSource) {
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
realDataSourceField.setAccessible(true);
realDataSource = (DataSource)realDataSourceField.get(dataSource);
}
}
if (null == realDataSource) {
realDataSource = dataSource;
}
if (null != realDataSource) {
DataSourceDestroyer destroyer = new DefaultDataSourceDestroyer();
if (graceDestroy) {
destroyer.asyncDestroy(ds, realDataSource);
} else {
destroyer.destroy(ds, realDataSource);
}
}
} catch (Exception e) {
log.warn("dynamic-datasource closed datasource named [{}] failed", ds, e);
}
}
public void setStrategy(final Class<? extends DynamicDataSourceStrategy> strategy) {
this.strategy = strategy;
}
public void setPrimary(final String primary) {
this.primary = primary;
}
public void setStrict(final Boolean strict) {
this.strict = strict;
}
public void setP6spy(final Boolean p6spy) {
this.p6spy = p6spy;
}
public void setSeata(final Boolean seata) {
this.seata = seata;
}
public void setGraceDestroy(final Boolean graceDestroy) {
this.graceDestroy = graceDestroy;
}
}
- DataSourceProperty (数据源配置参数)
java
package com.baomidou.dynamic.datasource.creator;
import com.baomidou.dynamic.datasource.creator.atomikos.AtomikosConfig;
import com.baomidou.dynamic.datasource.creator.beecp.BeeCpConfig;
import com.baomidou.dynamic.datasource.creator.dbcp.Dbcp2Config;
import com.baomidou.dynamic.datasource.creator.druid.DruidConfig;
import com.baomidou.dynamic.datasource.creator.hikaricp.HikariCpConfig;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSourceProperty {
private static final Logger log = LoggerFactory.getLogger(DataSourceProperty.class);
private String poolName;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
private Boolean seata = true;
private Boolean p6spy = true;
private Boolean lazy;
private DatasourceInitProperties init = new DatasourceInitProperties();
private DruidConfig druid = new DruidConfig();
private HikariCpConfig hikari = new HikariCpConfig();
private BeeCpConfig beecp = new BeeCpConfig();
private Dbcp2Config dbcp2 = new Dbcp2Config();
private AtomikosConfig atomikos = new AtomikosConfig();
private String publicKey;
public String getPoolName() {
return this.poolName;
}
public Class<? extends DataSource> getType() {
return this.type;
}
public String getDriverClassName() {
return this.driverClassName;
}
public String getUrl() {
return this.url;
}
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public String getJndiName() {
return this.jndiName;
}
public Boolean getSeata() {
return this.seata;
}
public Boolean getP6spy() {
return this.p6spy;
}
public Boolean getLazy() {
return this.lazy;
}
public DatasourceInitProperties getInit() {
return this.init;
}
public DruidConfig getDruid() {
return this.druid;
}
public HikariCpConfig getHikari() {
return this.hikari;
}
public BeeCpConfig getBeecp() {
return this.beecp;
}
public Dbcp2Config getDbcp2() {
return this.dbcp2;
}
public AtomikosConfig getAtomikos() {
return this.atomikos;
}
public String getPublicKey() {
return this.publicKey;
}
public void setPoolName(final String poolName) {
this.poolName = poolName;
}
public void setType(final Class<? extends DataSource> type) {
this.type = type;
}
public void setDriverClassName(final String driverClassName) {
this.driverClassName = driverClassName;
}
public void setUrl(final String url) {
this.url = url;
}
public void setUsername(final String username) {
this.username = username;
}
public void setPassword(final String password) {
this.password = password;
}
public void setJndiName(final String jndiName) {
this.jndiName = jndiName;
}
public void setSeata(final Boolean seata) {
this.seata = seata;
}
public void setP6spy(final Boolean p6spy) {
this.p6spy = p6spy;
}
public void setLazy(final Boolean lazy) {
this.lazy = lazy;
}
public void setInit(final DatasourceInitProperties init) {
this.init = init;
}
public void setDruid(final DruidConfig druid) {
this.druid = druid;
}
public void setHikari(final HikariCpConfig hikari) {
this.hikari = hikari;
}
public void setBeecp(final BeeCpConfig beecp) {
this.beecp = beecp;
}
public void setDbcp2(final Dbcp2Config dbcp2) {
this.dbcp2 = dbcp2;
}
public void setAtomikos(final AtomikosConfig atomikos) {
this.atomikos = atomikos;
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof DataSourceProperty)) {
return false;
} else {
DataSourceProperty other = (DataSourceProperty)o;
if (!other.canEqual(this)) {
return false;
} else {
Object this$seata = this.getSeata();
Object other$seata = other.getSeata();
if (this$seata == null) {
if (other$seata != null) {
return false;
}
} else if (!this$seata.equals(other$seata)) {
return false;
}
Object this$p6spy = this.getP6spy();
Object other$p6spy = other.getP6spy();
if (this$p6spy == null) {
if (other$p6spy != null) {
return false;
}
} else if (!this$p6spy.equals(other$p6spy)) {
return false;
}
Object this$lazy = this.getLazy();
Object other$lazy = other.getLazy();
if (this$lazy == null) {
if (other$lazy != null) {
return false;
}
} else if (!this$lazy.equals(other$lazy)) {
return false;
}
Object this$poolName = this.getPoolName();
Object other$poolName = other.getPoolName();
if (this$poolName == null) {
if (other$poolName != null) {
return false;
}
} else if (!this$poolName.equals(other$poolName)) {
return false;
}
Object this$type = this.getType();
Object other$type = other.getType();
if (this$type == null) {
if (other$type != null) {
return false;
}
} else if (!this$type.equals(other$type)) {
return false;
}
Object this$driverClassName = this.getDriverClassName();
Object other$driverClassName = other.getDriverClassName();
if (this$driverClassName == null) {
if (other$driverClassName != null) {
return false;
}
} else if (!this$driverClassName.equals(other$driverClassName)) {
return false;
}
Object this$url = this.getUrl();
Object other$url = other.getUrl();
if (this$url == null) {
if (other$url != null) {
return false;
}
} else if (!this$url.equals(other$url)) {
return false;
}
Object this$username = this.getUsername();
Object other$username = other.getUsername();
if (this$username == null) {
if (other$username != null) {
return false;
}
} else if (!this$username.equals(other$username)) {
return false;
}
Object this$password = this.getPassword();
Object other$password = other.getPassword();
if (this$password == null) {
if (other$password != null) {
return false;
}
} else if (!this$password.equals(other$password)) {
return false;
}
Object this$jndiName = this.getJndiName();
Object other$jndiName = other.getJndiName();
if (this$jndiName == null) {
if (other$jndiName != null) {
return false;
}
} else if (!this$jndiName.equals(other$jndiName)) {
return false;
}
Object this$init = this.getInit();
Object other$init = other.getInit();
if (this$init == null) {
if (other$init != null) {
return false;
}
} else if (!this$init.equals(other$init)) {
return false;
}
Object this$druid = this.getDruid();
Object other$druid = other.getDruid();
if (this$druid == null) {
if (other$druid != null) {
return false;
}
} else if (!this$druid.equals(other$druid)) {
return false;
}
Object this$hikari = this.getHikari();
Object other$hikari = other.getHikari();
if (this$hikari == null) {
if (other$hikari != null) {
return false;
}
} else if (!this$hikari.equals(other$hikari)) {
return false;
}
Object this$beecp = this.getBeecp();
Object other$beecp = other.getBeecp();
if (this$beecp == null) {
if (other$beecp != null) {
return false;
}
} else if (!this$beecp.equals(other$beecp)) {
return false;
}
Object this$dbcp2 = this.getDbcp2();
Object other$dbcp2 = other.getDbcp2();
if (this$dbcp2 == null) {
if (other$dbcp2 != null) {
return false;
}
} else if (!this$dbcp2.equals(other$dbcp2)) {
return false;
}
Object this$atomikos = this.getAtomikos();
Object other$atomikos = other.getAtomikos();
if (this$atomikos == null) {
if (other$atomikos != null) {
return false;
}
} else if (!this$atomikos.equals(other$atomikos)) {
return false;
}
Object this$publicKey = this.getPublicKey();
Object other$publicKey = other.getPublicKey();
if (this$publicKey == null) {
if (other$publicKey != null) {
return false;
}
} else if (!this$publicKey.equals(other$publicKey)) {
return false;
}
return true;
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof DataSourceProperty;
}
public int hashCode() {
int PRIME = 59;
int result = 1;
Object $seata = this.getSeata();
result = result * 59 + ($seata == null ? 43 : $seata.hashCode());
Object $p6spy = this.getP6spy();
result = result * 59 + ($p6spy == null ? 43 : $p6spy.hashCode());
Object $lazy = this.getLazy();
result = result * 59 + ($lazy == null ? 43 : $lazy.hashCode());
Object $poolName = this.getPoolName();
result = result * 59 + ($poolName == null ? 43 : $poolName.hashCode());
Object $type = this.getType();
result = result * 59 + ($type == null ? 43 : $type.hashCode());
Object $driverClassName = this.getDriverClassName();
result = result * 59 + ($driverClassName == null ? 43 : $driverClassName.hashCode());
Object $url = this.getUrl();
result = result * 59 + ($url == null ? 43 : $url.hashCode());
Object $username = this.getUsername();
result = result * 59 + ($username == null ? 43 : $username.hashCode());
Object $password = this.getPassword();
result = result * 59 + ($password == null ? 43 : $password.hashCode());
Object $jndiName = this.getJndiName();
result = result * 59 + ($jndiName == null ? 43 : $jndiName.hashCode());
Object $init = this.getInit();
result = result * 59 + ($init == null ? 43 : $init.hashCode());
Object $druid = this.getDruid();
result = result * 59 + ($druid == null ? 43 : $druid.hashCode());
Object $hikari = this.getHikari();
result = result * 59 + ($hikari == null ? 43 : $hikari.hashCode());
Object $beecp = this.getBeecp();
result = result * 59 + ($beecp == null ? 43 : $beecp.hashCode());
Object $dbcp2 = this.getDbcp2();
result = result * 59 + ($dbcp2 == null ? 43 : $dbcp2.hashCode());
Object $atomikos = this.getAtomikos();
result = result * 59 + ($atomikos == null ? 43 : $atomikos.hashCode());
Object $publicKey = this.getPublicKey();
result = result * 59 + ($publicKey == null ? 43 : $publicKey.hashCode());
return result;
}
public String toString() {
return "DataSourceProperty(poolName=" + this.getPoolName() + ", type=" + this.getType() + ", driverClassName=" + this.getDriverClassName() + ", url=" + this.getUrl() + ", username=" + this.getUsername() + ", password=" + this.getPassword() + ", jndiName=" + this.getJndiName() + ", seata=" + this.getSeata() + ", p6spy=" + this.getP6spy() + ", lazy=" + this.getLazy() + ", init=" + this.getInit() + ", druid=" + this.getDruid() + ", hikari=" + this.getHikari() + ", beecp=" + this.getBeecp() + ", dbcp2=" + this.getDbcp2() + ", atomikos=" + this.getAtomikos() + ", publicKey=" + this.getPublicKey() + ")";
}
}
- DynamicDataSourceContextHolder (数据源上下文)
java
package com.baomidou.dynamic.datasource.toolkit;
import java.util.ArrayDeque;
import java.util.Deque;
import org.springframework.core.NamedThreadLocal;
public final class DynamicDataSourceContextHolder {
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
protected Deque<String> initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
public static String peek() {
return (String)((Deque)LOOKUP_KEY_HOLDER.get()).peek();
}
public static String push(String ds) {
String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;
((Deque)LOOKUP_KEY_HOLDER.get()).push(dataSourceStr);
return dataSourceStr;
}
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();
}
}
- LoadBalanceDynamicDataSourceStrategy (数据源组选择策略-轮询)
java
package com.baomidou.dynamic.datasource.strategy;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
private final AtomicInteger index = new AtomicInteger(0);
public String determineKey(List<String> dsNames) {
return (String)dsNames.get(Math.abs(this.index.getAndAdd(1) % dsNames.size()));
}
}
- RandomDynamicDataSourceStrategy (数据源组选择策略-随机)
java
package com.baomidou.dynamic.datasource.strategy;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
public String determineKey(List<String> dsNames) {
return (String)dsNames.get(ThreadLocalRandom.current().nextInt(dsNames.size()));
}
}
- DynamicDataSourceAopConfiguration
java
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationAdvisor;
import com.baomidou.dynamic.datasource.aop.DynamicDataSourceAnnotationInterceptor;
import com.baomidou.dynamic.datasource.aop.DynamicLocalTransactionInterceptor;
import com.baomidou.dynamic.datasource.processor.DsHeaderProcessor;
import com.baomidou.dynamic.datasource.processor.DsProcessor;
import com.baomidou.dynamic.datasource.processor.DsSessionProcessor;
import com.baomidou.dynamic.datasource.processor.DsSpelExpressionProcessor;
import org.springframework.aop.Advisor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.context.expression.BeanFactoryResolver;
@Role(2)
@Configuration(
proxyBeanMethods = false
)
public class DynamicDataSourceAopConfiguration {
private final DynamicDataSourceProperties properties;
public DynamicDataSourceAopConfiguration(DynamicDataSourceProperties properties) {
this.properties = properties;
}
@Role(2)
@Bean
public static DynamicDataSourceProperties dynamicDataSourceProperties() {
return new DynamicDataSourceProperties();
}
@Role(2)
@Bean
@ConditionalOnMissingBean
public DsProcessor dsProcessor(BeanFactory beanFactory) {
DsProcessor headerProcessor = new DsHeaderProcessor();
DsProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
@Role(2)
@Bean
@ConditionalOnProperty(
prefix = "spring.datasource.dynamic.aop",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDatasourceAopProperties aopProperties = this.properties.getAop();
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(aopProperties.getAllowedPublicOnly(), dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class);
advisor.setOrder(aopProperties.getOrder());
return advisor;
}
@Role(2)
@Bean
@ConditionalOnProperty(
prefix = "spring.datasource.dynamic",
name = {"seata"},
havingValue = "false",
matchIfMissing = true
)
public Advisor dynamicTransactionAdvisor() {
DynamicDatasourceAopProperties aopProperties = this.properties.getAop();
DynamicLocalTransactionInterceptor interceptor = new DynamicLocalTransactionInterceptor(aopProperties.getAllowedPublicOnly());
return new DynamicDataSourceAnnotationAdvisor(interceptor, DSTransactional.class);
}
}
- DynamicDataSourceAutoConfiguration
java
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.util.CollectionUtils;
@Configuration
@AutoConfigureBefore(
value = {DataSourceAutoConfiguration.class},
name = {"com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"}
)
@Import({DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceAopConfiguration.class, DynamicDataSourceAssistConfiguration.class})
@ConditionalOnProperty(
prefix = "spring.datasource.dynamic",
name = {"enabled"},
havingValue = "true",
matchIfMissing = true
)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSourceAutoConfiguration.class);
private final DynamicDataSourceProperties properties;
private final List<DynamicDataSourcePropertiesCustomizer> dataSourcePropertiesCustomizers;
public DynamicDataSourceAutoConfiguration(DynamicDataSourceProperties properties, ObjectProvider<List<DynamicDataSourcePropertiesCustomizer>> dataSourcePropertiesCustomizers) {
this.properties = properties;
this.dataSourcePropertiesCustomizers = (List)dataSourcePropertiesCustomizers.getIfAvailable();
}
@Bean
@ConditionalOnMissingBean
public DataSource dataSource(List<DynamicDataSourceProvider> providers) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(providers);
dataSource.setPrimary(this.properties.getPrimary());
dataSource.setStrict(this.properties.getStrict());
dataSource.setStrategy(this.properties.getStrategy());
dataSource.setP6spy(this.properties.getP6spy());
dataSource.setSeata(this.properties.getSeata());
dataSource.setGraceDestroy(this.properties.getGraceDestroy());
return dataSource;
}
public void afterPropertiesSet() {
if (!CollectionUtils.isEmpty(this.dataSourcePropertiesCustomizers)) {
for(DynamicDataSourcePropertiesCustomizer customizer : this.dataSourcePropertiesCustomizers) {
customizer.customize(this.properties);
}
}
}
}
- DefaultDataSourceCreator
java
package com.baomidou.dynamic.datasource.creator;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.enums.SeataMode;
import com.baomidou.dynamic.datasource.event.DataSourceInitEvent;
import com.baomidou.dynamic.datasource.support.ScriptRunner;
import com.baomidou.dynamic.datasource.toolkit.DsStrUtils;
import com.p6spy.engine.spy.P6DataSource;
import io.seata.rm.datasource.DataSourceProxy;
import io.seata.rm.datasource.xa.DataSourceProxyXA;
import java.util.List;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultDataSourceCreator {
private static final Logger log = LoggerFactory.getLogger(DefaultDataSourceCreator.class);
private List<DataSourceCreator> creators;
private Boolean lazy = false;
private Boolean p6spy = false;
private Boolean seata = false;
private SeataMode seataMode;
private String publicKey;
private DataSourceInitEvent dataSourceInitEvent;
public DefaultDataSourceCreator() {
this.seataMode = SeataMode.AT;
this.publicKey = "MFwwDQ**********AwEAAQ==";
}
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
DataSourceCreator dataSourceCreator = null;
for(DataSourceCreator creator : this.creators) {
if (creator.support(dataSourceProperty)) {
dataSourceCreator = creator;
break;
}
}
if (dataSourceCreator == null) {
throw new IllegalStateException("creator must not be null,please check the DataSourceCreator");
} else {
String propertyPublicKey = dataSourceProperty.getPublicKey();
if (DsStrUtils.isEmpty(propertyPublicKey)) {
dataSourceProperty.setPublicKey(this.publicKey);
}
Boolean propertyLazy = dataSourceProperty.getLazy();
if (propertyLazy == null) {
dataSourceProperty.setLazy(this.lazy);
}
if (this.dataSourceInitEvent != null) {
this.dataSourceInitEvent.beforeCreate(dataSourceProperty);
}
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
if (this.dataSourceInitEvent != null) {
this.dataSourceInitEvent.afterCreate(dataSource);
}
this.runScrip(dataSource, dataSourceProperty);
return this.wrapDataSource(dataSource, dataSourceProperty);
}
}
private void runScrip(DataSource dataSource, DataSourceProperty dataSourceProperty) {
DatasourceInitProperties initProperty = dataSourceProperty.getInit();
String schema = initProperty.getSchema();
String data = initProperty.getData();
if (DsStrUtils.hasText(schema) || DsStrUtils.hasText(data)) {
ScriptRunner scriptRunner = new ScriptRunner(initProperty.isContinueOnError(), initProperty.getSeparator());
if (DsStrUtils.hasText(schema)) {
scriptRunner.runScript(dataSource, schema);
}
if (DsStrUtils.hasText(data)) {
scriptRunner.runScript(dataSource, data);
}
}
}
private DataSource wrapDataSource(DataSource dataSource, DataSourceProperty dataSourceProperty) {
String name = dataSourceProperty.getPoolName();
DataSource targetDataSource = dataSource;
Boolean enabledP6spy = this.p6spy && dataSourceProperty.getP6spy();
if (enabledP6spy) {
targetDataSource = new P6DataSource(dataSource);
log.debug("dynamic-datasource [{}] wrap p6spy plugin", name);
}
Boolean enabledSeata = this.seata && dataSourceProperty.getSeata();
if (enabledSeata) {
if (SeataMode.XA == this.seataMode) {
targetDataSource = new DataSourceProxyXA(targetDataSource);
} else {
targetDataSource = new DataSourceProxy(targetDataSource);
}
log.debug("dynamic-datasource [{}] wrap seata plugin transaction mode ", name);
}
return new ItemDataSource(name, dataSource, targetDataSource, enabledP6spy, enabledSeata, this.seataMode);
}
public void setCreators(final List<DataSourceCreator> creators) {
this.creators = creators;
}
public void setLazy(final Boolean lazy) {
this.lazy = lazy;
}
public void setP6spy(final Boolean p6spy) {
this.p6spy = p6spy;
}
public void setSeata(final Boolean seata) {
this.seata = seata;
}
public void setSeataMode(final SeataMode seataMode) {
this.seataMode = seataMode;
}
public void setPublicKey(final String publicKey) {
this.publicKey = publicKey;
}
public void setDataSourceInitEvent(final DataSourceInitEvent dataSourceInitEvent) {
this.dataSourceInitEvent = dataSourceInitEvent;
}
}
十八、多数据源的事务管理
待续...