dynamic-datasource-spring-boot-starter
是一个基于springboot
的快速集成多数据源的启动器。
特性
- 支持
数据源分组
,适用于多种场景,纯粹多库、读写分离、一主多从、混合模式。 - 支持数据库敏感配置信息
加密(可自定义)
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的分布式事务
方案。 - 提供
本地多数据源事务
方案。
约定
dynamic-datasource
只做切换数据源
这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD
。- 配置文件所有以下划线
_
分割的数据源首部
即为组的名称,相同组名称的数据源会放在一个组下。 - 切换数据源可以是
组名
,也可以是具体数据源名称
。组名则切换时采用负载均衡算法切换。 - 默认的数据源名称为
master
,你可以通过spring.datasource.dynamic.primary
修改。 - 方法上的注解优先于类上注解。
DS
支持继承抽象类上的DS
,暂不支持继承接口上的DS
。
使用方法
1、引入dynamic-datasource-spring-boot-starter
spring-boot 1.5.x、2.x.x
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
spring-boot3及以上
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${version}</version>
</dependency>
2、配置数据源
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.cj.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.cj.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.cj.jdbc.Driver
# ......省略
# 以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从
spring:
datasource:
dynamic:
datasource:
master_1:
master_2:
slave_1:
slave_2:
slave_3:
# 纯粹多库(记得设置primary)
spring:
datasource:
dynamic:
datasource:
mysql:
oracle:
sqlserver:
postgresql:
h2:
# 混合配置
spring:
datasource:
dynamic:
datasource:
master:
slave_1:
slave_2:
oracle_1:
oracle_2:
3、使用@DS
切换数据源
@DS
可以注解在方法上或类上,同时存在就近原则,方法上注解
优先于类上注解
。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS("dsName") | dsName 可以为组名也可以为具体某个库的名称,组名则切换时采用负载均衡算法切换。 |
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired private JdbcTemplate jdbcTemplate;
// 使用slave数据源
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
// 使用slave_1数据源
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
DS放在哪里合适
DS
作为切换数据源核心注解,我应该把他注解在哪里合适?
这其实是初次接触多数据源的人常问的问题。
这其实没有一定的要求,只是有一些经验之谈。
首先开发者要了解的基础知识是,DS
注解是基于AOP
的原理实现的,aop
的常见失效场景应清楚,比如内部调用失效,shiro
代理失效。
通常建议DS
放在serviceImpl
的方法上,如事务注解一样。
注解在Controller的方法上或类上
并不是不可以,作者并不建议的原因主要是controller
主要作用是参数的检验等一些基础逻辑的处理,这部分操作常常并不涉及数据库。
注解在service的实现类的方法或类上
这是作者建议的方式,service
主要是对业务的处理,在复杂的场景涉及连续切换不同的数据库。
如果你的方法有通用性,其他service
也会调用你的方法。 这样别人就不用重复处理切换数据源。
注解在Mapper上
通常如果你某个Mapper
对应的表只在确定的一个库,也是可以的,但是建议只注解在Mapper
的类上。
我之前出现多线程失效场景的时候,就是在Mapper
上加了注解解决的。
其他使用方式
继承抽象类上的DS
比如我有一个抽象Service
,我想实现继承我这个抽象Service
下的子Service
的所有方法除非重新指定,都用我抽象Service
上注解的数据源。是否支持?
答:支持。
继承接口上的DS
3.4.1开始支持,但是需要注意的是,一个类能实现多个接口,如果多个接口都有DS会如何?
不知道,别这么干。一般不会有人这么干吧,想一想都知道会出问题。
连接池集成
传统多数据源集成连接池如果需要配置的参数较多,则手动编码量大,编程复杂。
dynamic-datasource
实现了常见数据源的参数配置,支持全局配置每个数据源继承。
通过本章您可以快速掌握不同连接池的集成配置方案和继承中可能遇见的问题。
连接池必读
每个数据源都有一个type
来指定连接池。
每个数据源甚至可以使用不同的连接池,如无特殊需要并不建议。
type
不是必填字段。
在没有设置type的时候系统会自动按以下顺序查找并使用连接池:Druid > HikariCp > BeeCp > DBCP2 > Spring Basic
。
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.cj.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.cj.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.cj.jdbc.Driver
type: cn.beecp.BeeDataSource # 使用beecp
集成Druid
基础介绍
Druid Github:点我跳转
Druid 文档:点我跳转
dynamic-datasource
能简单高效完成对Druid
的集成并完成其参数的多元化配置。
各个库可以使用不同的数据库连接池,如master
使用Druid
,slave
使用HikariCP
。
如果项目同时存在Druid
和HikariCP
并且未配置连接池type
类型,默认Druid
优先于HikariCP
。
集成步骤
1、项目引入druid-spring-boot-starter
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
2、排除原生Druid
的快速配置类
注意:v3.3.3
及以上版本不用排除了。
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
某些SpringBoot的版本上面可能无法排除可用以下方式排除。
spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
3、参数配置
- 如果参数都未配置,则保持原组件默认值。
- 如果配置了全局参数,则每一个数据源都会继承对应参数。
- 每一个数据源可以单独设置参数覆盖全局参数。
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 12345678
dynamic:
druid: # 以下是支持的全局默认值
initial-size:
max-active:
min-idle:
max-wait:
time-between-eviction-runs-millis:
time-between-log-stats-millis:
stat-sqlmax-size:
min-evictable-idle-time-millis:
max-evictable-idle-time-millis:
test-while-idle:
test-on-borrow:
test-on-return:
validation-query:
validation-query-timeout:
use-global-datasource-stat:
async-init:
clear-filters-enable:
reset-stat-enable:
not-full-timeout-retry-count:
max-wait-thread-count:
fail-fast:
phyTimeout-millis:
keep-alive:
pool-prepared-statements:
init-variants:
init-global-variants:
use-unfair-lock:
kill-when-socket-read-timeout:
connection-properties:
max-pool-prepared-statement-per-connection-size:
init-connection-sqls:
share-prepared-statements:
connection-errorretry-attempts:
break-after-acquire-failure:
filters: stat # 注意这个值和druid原生不一致,默认启动了stat
wall:
noneBaseStatementAllow:
callAllow:
selectAllow:
selectIntoAllow:
selectIntoOutfileAllow:
selectWhereAlwayTrueCheck:
selectHavingAlwayTrueCheck:
selectUnionCheck:
selectMinusCheck:
selectExceptCheck:
selectIntersectCheck:
createTableAllow:
dropTableAllow:
alterTableAllow:
renameTableAllow:
hintAllow:
lockTableAllow:
startTransactionAllow:
blockAllow:
conditionAndAlwayTrueAllow:
conditionAndAlwayFalseAllow:
conditionDoubleConstAllow:
conditionLikeTrueAllow:
selectAllColumnAllow:
deleteAllow:
deleteWhereAlwayTrueCheck:
deleteWhereNoneCheck:
updateAllow:
updateWhereAlayTrueCheck:
updateWhereNoneCheck:
insertAllow:
mergeAllow:
minusAllow:
intersectAllow:
replaceAllow:
setAllow:
commitAllow:
rollbackAllow:
useAllow:
multiStatementAllow:
truncateAllow:
commentAllow:
strictSyntaxCheck:
constArithmeticAllow:
limitZeroAllow:
describeAllow:
showAllow:
schemaCheck:
tableCheck:
functionCheck:
objectCheck:
variantCheck:
mustParameterized:
doPrivilegedAllow:
dir:
tenantTablePattern:
tenantColumn:
wrapAllow:
metadataAllow:
conditionOpXorAllow:
conditionOpBitwseAllow:
caseConditionConstAllow:
completeInsertValuesCheck:
insertValuesCheckSize:
selectLimit:
stat:
merge-sql:
log-slow-sql:
slow-sql-millis:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.cj.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,推荐使用本项目自带的加密方法。
以下是我曾经配置过的示例:
# 数据源配置
spring:
# 排除掉druid原生的自动配置
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
datasource:
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: admin
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 2000
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic:
druid:
# 初始连接数
initial-size: 5
# 最小连接池数量
min-idle: 10
# 最大连接池数量
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
max-evictable-idle-time-millis: 900000
# 配置检测连接是否有效
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并指定每个连接上PSCache的大小。
# oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
filters: stat # 注意这个值和druid原生不一致,默认启动了stat
stat:
merge-sql: true
log-slow-sql: true
slow-sql-millis: 2000
primary: master # 设置默认的数据源或者数据源组
strict: false # 设置严格模式,默认false不启动,启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源
datasource:
master:
url: jdbc:mysql://192.168.56.101:3306/master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_1:
url: jdbc:mysql://192.168.56.102:3306/slave_1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2:
url: jdbc:mysql://192.168.56.103:3306/slave_2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid: # 以下参数针对每个库可以重新设置druid参数
initial-size:
validation-query: select 1 FROM DUAL # 比如oracle就需要重新设置这个
public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter。
如上即可配置访问用户和密码,访问http://ip:端口/druid/index.html
查看druid
监控。
示例项目
核心源码
Druid数据源创建器:点我跳转
Druid参数源码:点我跳转
集成HikariCP
基础介绍
HikariCP Github:点我跳转
HikariCP 文档:点我跳转
集成步骤
1、项目引入HikariCP
依赖
SpringBoot2.x.x
默认引入了HikariCP
,除非对版本有要求无需再次引入。
SpringBoot 1.5.x
需手动引入,对应的版本请根据自己环境和HikariCP
官方文档自行选择。
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${version}</version>
</dependency>
2、参数配置
- 如果参数都未配置,则保持原组件默认值。
- 如果配置了全局参数,则每一个数据源都会继承对应参数。
- 每一个数据源可以单独设置参数覆盖全局参数。
特别注意,hikaricp
原生设置某些字段名和dynamic-datasource
不一致,dynamic-datasource
是根据参数反射设置,而原生hikaricp
字段名称和set
名称不一致。
spring:
datasource:
dynamic:
hikari: # 全局hikariCP参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
catalog:
connection-timeout:
validation-timeout:
idle-timeout:
leak-detection-threshold:
max-lifetime:
max-pool-size:
min-idle:
initialization-fail-timeout:
connection-init-sql:
connection-test-query:
dataSource-class-name:
dataSource-jndi-name:
schema:
transaction-isolation-name:
is-auto-commit:
is-read-only:
is-isolate-internal-queries:
is-register-mbeans:
is-allow-pool-suspension:
data-source-properties:
health-check-properties:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
hikari: # 以下参数针对每个库可以重新设置hikari参数
max-pool-size:
idle-timeout:
# ......
常见问题
Failed to validate connection com.mysql.jdbc.JDBC4Connection@xxxxx (No operations allowed after connection closed.)
https://github.com/brettwooldridge/HikariCP/issues/1034
核心意思就是HikariCP
要设置connection-test-query
并且max-lifetime
要小于mysql
的默认时间。
核心源码
HikariCP数据源创建器:点我跳转
HikariCP参数配置类:点我跳转
第三方集成
通过本章您可以掌握数据源在集成MybatisPlus
,Quartz
,ShardingJdbc
的方案。
集成MybatisPlus
基础介绍
MybatisPlus Github:点我跳转
MybatisPlus 文档 点我跳转
只要引入了
MybatisPlus
相关jar
包,项目自动集成,兼容MybatisPlus2.x
和3.x
的版本。
集成步骤
1、项目引入Mybatis-Plus
依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${version}</version>
</dependency>
2、使用DS
注解进行切换数据源
@Service
@DS("mysql")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@DS("oracle")
publid void addUser(User user){
//do something
baseMapper.insert(user);
}
}
3、分页配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//如果是不同类型的库,请不要指定DbType,其会自动判断。
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
注意事项
MybatisPlus
内置的ServiceImpl
在新增,更改,删除等一些方法上自带事务导致不能切换数据源。
解决方法:
方法1:复制ServiceImpl
出来为自己的MyServiceImpl
,并去掉所有事务注解。
方法2:创建一个新方法,并在此方法上加DS
注解. 如上面的addUser
方法。
FAQ
为什么要单独拿出来说和MybatisPlus的集成?
因为MybatisPlus重写了一些核心类,必须通过解析获得真实的代理对象。
如果自己写多数据源,则很难完成与mp的集成。
核心解析源码:点我跳转
示例项目
其他集成
集成P6spy、集成Quartz、集成ShardingJdbc
由于以上场景暂时没用到,文章中暂时不体现了,如有用到,后面会补充上。
进阶使用
动态添加移除数据源
:指在系统运行过程中动态的添加数据源,删除数据源,多使用于基于数据库的多租户系统。
动态解析数据源
:指数据源切换是不固定的,可以根据域名,根据header
参数,根据session
参数,根据方法变量等来动态切换。多使用于多租户系统,支持扩展。
数据库加密
:指数据库的url
,username
,password
需要加密,长用于安全性较高系统,不让普通开发通过配置知道生产库的连接配置。
启动初始化脚本
:指数据源启动的时候可以执行schema
和data
的脚本来初始化结构和数据,dynamic-datasource
组件支持每个数据源加载不同的schema
和data
。
自动读写分离
:适用于mybatis
环境,基于mybatis
插件实现无注解自动读写分离。
懒启动数据源
:指配置的数据源不立即启动,等需要建立连接的时候再真正初始化连接池。
无数据源启动
:指启动的时候不配置任何数据源,全靠后期系统动态添加。
手动切换数据源
:指某些情况无法根据注解切换,通过工具类手动切换数据源。
动态添加移除数据源
基础介绍
主要在多租户场景中,常常新的一个租户进来需要动态的添加一个数据源到库中,使得系统不用重启即可切换数据源。
使用步骤
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;
@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 "删除成功";
}
}
示例项目
源码分析
public interface DataSourceCreator {
/**
* 通过属性创建数据源
*
* @param dataSourceProperty 数据源属性
* @return 被创建的数据源
*/
DataSource createDataSource(DataSourceProperty dataSourceProperty);
/**
* 当前创建器是否支持根据此属性创建
*
* @param dataSourceProperty 数据源属性
* @return 是否支持
*/
boolean support(DataSourceProperty dataSourceProperty);
}
DataSourceCreator
是一个接口,定义了根据参数创建数据源的接口。
其他creator
实现此接口,dynamic-datasource
项目暂时实现了Druid
和Hikaricp
的等连接池的实现。
BasicDataSourceCreator
是调用Spring
原生的创建方式,只支持最最原始的基础配置。
DefaultDataSourceCreator
是一个通用的创建器,其根据环境自动选择连接池。
动态解析数据源
基础介绍
默认有三个职责链来处理动态参数解析器header -> session -> spel
所有以#
开头的参数都会从参数中获取数据源
@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();
}
如何扩展
-
我想从
cookie
中获取参数解析? -
我想从其他环境属性中来计算?
参考header解析器,继承DsProcessor
,如果matches
返回true
则匹配成功,调用doDetermineDatasource
返回匹配到的数据源,否则跳到下一个解析器。
1、自定义一个处理器
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();
return request.getHeader(key.substring(8));
}
}
2、重写完后重新注入一个根据自己解析顺序的解析处理器
@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
接口下面的方法使用:
public interface UserMapper{
// 前缀可以是p0,a0
@DS("#p0.tenantName")
public List selecSpelByTenant(User user);
}
对于在接口下面的使用, 由于编译器的默认配置没有将接口参数的元数据写入字节码文件中,所以spring el会无法识别参数名称, 只能用默认的参数命名方式
- 第一个参数: p0,a0,(加入-parameters后,可以使用参数具体的名字,例如这里的#user)
- 第二个参数: p1,a1
- 第三个参数: P2,,a2
可以通过修改maven
配置和java
编译配置将接口参数信息写入字节码文件
<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
java
支持-parameters
的最低版本为1.8
数据库加密
基础介绍
在一些项目中,有对数据库关键字段加密的需求,大家熟悉的是Druid
的加密方式。
在连接池集成中的Druid
章节里有对应的加密方式,但是如果我不用Druid
也想用加密呢?
所以作者copy
了Druid
的加密相关源码,嘿嘿。
dynamic-datasource
项目也支持支持url
,username
,password
的加密。
使用的RAS
加密,相关原理文章 https://www.cnblogs.com/pcheng/p/9629621.html。
简单来说就是生成两把钥匙,私钥加密,公钥解密。
公钥可以发布出去,解密也是用的公钥。
具体使用
1、获得加密字符串
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
public class Demo {
public static void main(String[] args) throws Exception {
String password = "123456";
// 使用默认的publicKey ,建议还是使用下面的自定义
String encodePassword = CryptoUtils.encrypt(password);
System.out.println(encodePassword);
}
// 自定义publicKey
public static void main(String[] args) throws Exception {
String[] arr = CryptoUtils.genKeyPair(512);
System.out.println("privateKey: " + arr[0]);
System.out.println("publicKey: " + arr[1]);
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"));
}
}
2、配置加密yml
ENC(xxx)
中包裹的xxx
即为使用上面加密方法后生成的字符串
spring:
datasource:
dynamic:
public-key: # 有默认值,强烈建议更换
datasource:
master:
url: ENC(xxx)
username: ENC(xxx)
password: ENC(xxx)
driver-class-name: com.mysql.cj.jdbc.Driver
public-key: # 每个数据源可以独立设置,没有就继承上面的。
自定义解密
一些公司要求使用自己的方式加密,解密。
从3.5.0
版本开始,扩展了一个event
,用户自行实现注入即可。
public interface DataSourceInitEvent {
/**
* 连接池创建前执行(可用于参数解密)
*
* @param dataSourceProperty 数据源基础信息
*/
void beforeCreate(DataSourceProperty dataSourceProperty);
/**
* 连接池创建后执行
*
* @param dataSource 连接池
*/
void afterCreate(DataSource dataSource);
}
默认的实现是EncDataSourceInitEvent
,即ENC
方式的。
为什么不是公钥加密,私钥解密
根据RSA
的设计,大部分人会认为应该是公钥加密,私钥解密。 为什么Druid
设计相反?
建议更高的安全,可以把publicKey
在启动时候传进去,或者配置中心配好,不让普通开发接触到就好。
查询了Druid
的ISSUE
和一些文章:
1、Druid作者wenshao自己的回答:点我跳转
2、知乎一些文章片段
启动初始化执行脚本
3.5.0
之前版本
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
一致)
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默认分号分隔符,一般无需更改
懒启动数据源
懒启动:连接池创建出来后并不会立即初始化连接池,等需要使用connection
的时候再初始化。
暂时只支持Druid
和HikariCp
和BeeCp
连接池。
主要场景可能适合于数据源很多,又不需要启动立即初始化的情况,可以减少系统启动时间。
缺点
在于,如果参数配置有误,则启动的时候不知道,初始化的时候失败,可能一直抛异常。
配置使用
spring:
datasource:
dynamic:
primary: master # 设置默认的数据源或者数据源组,默认值即为master
strict: false # 设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
lazy: true # 默认false非懒启动,系统加载到数据源立即初始化连接池
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
lazy: true #表示这个数据源懒启动
db1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
db2:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
无数据源启动
基础介绍
场景:部分系统可能启动的时候完全不需要数据源,全靠启动后动态添加。
配置方式
即基本不需要配置,可能根据需要配置一些类似Druid
和HikariCp
全局参数。
如需配置Druid
和HikariCp
全局参数可参考对应章节文档。
spring:
datasource:
dynamic:
primary: master # 设置默认的数据源或者数据源组,默认值即为master
strict: false # 设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
因为没有匹配的主数据源,启动的时候你会见到类似如下日志。
dynamic-datasource initial loaded 0 datasource,Please add your primary datasource or check your configuration
手动切换数据源
在某些情况您可能需要手动切换数据源。
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()
清空当前线程的数据源信息,防止内存泄漏,因为一层使用的是ThreadLocal
如果你不太清楚什么时候调用,那么可以参考下面写一个拦截器,注册进spring
里即可。
@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();
}
}
手动注入多数据源
什么时候需要手动注入多数据源?
绝大部分是因为您的系统需要和其他数据源共同存在使用。
如Quartz
和ShardingJdbc
等都需要使用独立的数据源。
dynamic-datasource 3.4.0
及以上版本和老版本注入方式有一定差别,根据自己版本注入。
注意一定要让多数据源使用@Primary
,让其成为主数据源。
// 3.4.0版本以下
@Primary
@Bean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
// 3.4.0版本及以上
@Primary
@Bean
public 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;
}
主要变更是因为3.4.0
支持了多个provider
同时生效,采取了内部注入,具体源码改动。
自定义
自定义注解:不满足于默认DS
注解,希望自定义注解。
自定义数据源来源:默认的数据源来源是基于yml
或者properties
配置来的,组件支持同时从多个地方来加载初始化数据源。常用于多租户系统,从一个租户信息库多加载不同租户的数据源。
自定义负载均衡策略:常用的有轮询,随机。支持扩展。
自定义切面:支持不使用注解,通过配置来切换数据源。如某个包下所有service
方法以select*
开头的都走slave
库,以add*
开头的都走master
库。
自定义注解
基础介绍
如果你只有几个确定的库,可以尝试自定义注解替换掉@DS
。
建议从3.2.1
版本开始使用自定义注解,另外组件自带了@Master
和@Slave
注解。
使用方法
下面我们自定义一个产品库的注解,方便我们后面程序的使用。
1、需要自己定义一个注解并继承自DS
import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("product")
public @interface Product {
}
2、注解在需要切换数据源的方法上或类上
@Service
@Product
public class ProductServiceImpl implements ProductService {
@Product
public List selectAll() {
return jdbcTemplate.queryForList("select * from products");
}
}
自定义数据源来源
基础介绍
数据源来源的默认实现是YmlDynamicDataSourceProvider
,其从yaml
或properties
中读取信息并解析出所有数据源信息。
public interface DynamicDataSourceProvider {
/**
* 加载所有数据源
*
* @return 所有数据源,key为数据源名称
*/
Map<String, DataSource> loadDataSources();
}
自定义示例
可以参考AbstractJdbcDataSourceProvider
(仅供参考)来实现从JDBC
数据库中获取数据库连接信息。
@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {
return new AbstractJdbcDataSourceProvider("com.mysql.cj.jdbc.Driver", "jdbc:mysql://xx.xx.xx.xx:3306/dynamic", "root", "root") {
@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
随机的。
spring:
datasource:
dynamic:
datasource:
master:
username: root
password: root
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
driver-class-name: com.mysql.cj.jdbc.Driver
schema: db/schema.sql
slave_1:
username: root
password: root
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
driver-class-name: com.mysql.cj.jdbc.Driver
slave_2:
username: root
password: root
url: jdbc:mysql://xx.xx.xx.xx:3308/dynamic
driver-class-name: com.mysql.cj.jdbc.Driver
slave_3:
username: root
password: root
url: jdbc:mysql://xx.xx.xx.xx:3309/dynamic
driver-class-name: com.mysql.cj.jdbc.Driver
strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy
如何自定义
如果默认的两个都不能满足要求,可以参考源码自定义,暂时只能全局更改。
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()));
}
}
无注解方案
不管是出于注解侵入性还是代码整洁等理由,我们都有需求不想使用注解的需求。
但是需要理解多数据源实现的核心。
public class DynamicRoutingDataSource {
@Override
public DataSource determineDataSource() {
// 这里是核心,是从ThreadLocal中获取当前数据源。
String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}
所以我们就可以根据需求,选择合适的时机调用DynamicDataSourceContextHolder.push("对应数据源")
。
默认的@DS
注解来切换数据源是根据spring AOP
的特性,在方法开启前设置数据源KEY
,方法执行完成后移除对应数据源KEY
。
filter切换数据源
目标:拦截以filter/**开头的所有请求,如果后面的方法以a开头就切换到db1,以b开头就切换到db2,其余使用默认数据源。
实现如下:
@Slf4j
@WebFilter(filterName = "dsFilter", urlPatterns = {"/filter/*"})
public class DynamicDatasourceFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("loading filter {}", filterConfig.getFilterName());
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI();
log.info("经过多数据源filter,当前路径是{}", requestURI);
// String headerDs = request.getHeader("ds");
// Object sessionDs = request.getSession().getAttribute("ds");
String s = requestURI.replaceFirst("/filter/", "");
String dsKey = "master";
if (s.startsWith("a")) {
dsKey = "db1";
} else if (s.startsWith("b")) {
dsKey = "db2";
} else {
}
// 执行
try {
DynamicDataSourceContextHolder.push(dsKey);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
@Override
public void destroy() {
}
}
@SpringBootApplication
@ServletComponentScan // filter必须使用这个
@MapperScan("com.baomidou.samples.ds.mapper")
public class AllDataSourceApplication {
public static void main(String[] args) {
SpringApplication.run(AllDataSourceApplication.class, args);
}
}
扩展:从
request
中还能获取到很多东西,如header
和session
,同理可以根据自己业务需求根据header
值和session
里对应的用户来动态设置和切换数据源。
intercepror切换数据源
目标:拦截以interceptor/**
开头的所有请求,如果后面的方法以a开头就切换到db1
,以b开头就切换到db2
,其余使用默认数据源。
实现如下:
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();
}
}
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/**");
}
}
自定义切面
从3.4.0
开始支持自定义切面。
- 使用这个方式,原注解方式并不会失效。
- 注意:不要在同一个切面同时使用注解又使用自定义切面。
@Configuration
public class MyConfig {
@Bean
public DynamicDatasourceNamedInterceptor dsAdvice(DsProcessor dsProcessor) {
DynamicDatasourceNamedInterceptor interceptor = new DynamicDatasourceNamedInterceptor(dsProcessor);
Map<String, String> patternMap = new HashMap<>();
patternMap.put("select*", "slave");
patternMap.put("add*", "master");
patternMap.put("update*", "master");
patternMap.put("delete*", "master");
interceptor.addPatternMap(patternMap);
return interceptor;
}
@Bean
public Advisor dsAdviceAdvisor(DynamicDatasourceNamedInterceptor dsAdvice) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution (* com.baomidou.samples.pattern..service.*.*(..))");
return new DefaultPointcutAdvisor(pointcut, dsAdvice);
}
}
以上实现com.baomidou.samples.pattern
包service
下所有类的add/update/delete
开头的方法使用master
数据源,select
使用slave
数据源。
更复杂的切点表达式语法需自行学习。
mybatis下读写分离
场景:
- 在纯的读写分离环境,写操作全部是
master
,读操作全部是slave
。 - 不想通过注解配置完成以上功能。
答:在mybatis
环境下可以基于mybatis
插件结合dynamic-datasource
完成以上功能。
手动注入插件:
@Bean
public MasterSlaveAutoRoutingPlugin masterSlaveAutoRoutingPlugin(){
return new MasterSlaveAutoRoutingPlugin();
}
默认主库名称master
,从库名称slave
。
暂不支持更多,源码简单可参考重写。
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Slf4j
public class MasterSlaveAutoRoutingPlugin implements Interceptor {
private static final String MASTER = "master";
private static final String SLAVE = "slave";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
try {
DynamicDataSourceContextHolder.push(SqlCommandType.SELECT == ms.getSqlCommandType() ? SLAVE : MASTER);
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.clear();
}
}
@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}
@Override
public void setProperties(Properties properties) {
}
}