动态数据源切换
前提
SpringBoot本身是可以配置多个数据源的,但是SpringBoot的多数据做不到动态的切换,只能在代码里面通过注解或写死。
一般的情况下作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程
Mybatis配置多数据源是使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,这样的入侵性比较明显
1
2
3
4
5
6
7
8
9
10
11private UserMapperPrimary userMapperPrimary;
private UserMapperSecondary userMapperSecondary;
// 往Primary数据源插入一条数据
userMapperPrimary.insert("AAA", 20);
// 从Primary数据源查询刚才插入的数据,配置正确就可以查询到
UserPrimary userPrimary = userMapperPrimary.findByName("AAA");
// 从Secondary数据源查询刚才插入的数据,配置正确应该是查询不到的
UserSecondary userSecondary = userMapperSecondary.findByName("AAA");使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样的入侵性较低,非常好的满足使用的需求。
场景
使用场景
单数据源的场景(一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求)
多数据源多SessionFactory这样的场景,估计作为刚刚开始想象想处理在使用框架的情况下处理业务,配置多个SessionFactory,然后在Dao层中对于特定的请求,通过特定的SessionFactory即可处理实现这样的业务需求,不过这样的处理带来了很多的不便之处,所有很多情况下我们宁愿直接使用封装的JDBC编程,或者使用Mybatis处理这样的业务场景
使用AbstractRoutingDataSource 的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource,这样的编写方式比较好
- 编写AbstractRoutingDataSource的实现类
- HandlerDataSource就是提供给我们动态选择数据源的数据的信息
- 我们编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以类中手动的设置当前的数据源
实现
配置依赖
Springboot+mybatis+jdbc+druid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 引入 mybatis 场景启动器 -->
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 引入JDBC场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 数据库连接依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 引入 druid 场景启动器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- druid如果启用日志记录时,需要导入Log4j 依赖-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
</dependencies>
配置文件
yaml文件如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89spring:
aop:
# 决定是基于接口的还是基于类的代理被创建
# 默认为false,表示使用JDK动态代理织入增强;当值为true时,表示使用CGLib动态代理织入增强
# 如果目标类没有生命接口,则Spring将自动使用CGLib动态代理.
proxy-target-class: true
datasource:
#readSize为从库数量
readSize: 1
type: com.alibaba.druid.pool.DruidDataSource
druid:
aop-patterns: tk.fulsun.demo.service.*,tk.fulsun.demo.dao.*,tk.fulsun.demo.controller.*,tk.fulsun.demo.mapper.*
master:
url: jdbc:mysql://192.168.56.101:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
# 5.7版本是 com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
username: test
password: 123456
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
min-evictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: true
testOnReturn: false
poolPreparedStatements: true
useGlobalDataSourceStat: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,log4j2
WebStatFilter:
enabled: true
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
urlPattern: '/*'
StatViewServlet:
enabled: true
urlPattern: '/druid/*'
loginUsername: druid
loginPassword: druid
reset-enable: false
allow: 127.0.0.1
slave:
url: jdbc:mysql://192.168.56.101:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.jdbc.Driver
username: test
password: 123456
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
min-evictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: true
testOnReturn: false
poolPreparedStatements: true
useGlobalDataSourceStat: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall
WebStatFilter:
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
enabled: true
urlPattern: '/*'
StatViewServlet:
enabled: true
urlPattern: '/druid/*'
loginUsername: druid
loginPassword: druid
server:
port: 8888
mybatis:
# 映射实体地址
type-aliases-package: tk.fulsun.demo.entity
# xml配置文件地址
mapper-locations: classpath:mapper/*.xml
# mybatis全局配置,与configuration 不能同时存在
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
# 开启驼峰命名
map-underscore-to-camel-case: true
#当传入null的时候对应的jdbctype
jdbc-type-for-null: null
#用map接受查询结果时,会自动将查询结果为null的字段忽略
#查询到0条记录时 会接收到一个所有key值都为null的map
#只查询一个字段,而用map接收 会接收到一个为null的map
call-setters-on-nulls: true
动态数据源选择
AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Description AbstractRoutingDataSource(每执行一次数据库,动态获取DataSource)
* @Date 2021/6/14
* @Created by 凉月-文
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
管理数据源
设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import java.util.ArrayList;
import java.util.List;
/**
* @Description 动态数据源上下文管理
* @Date 2021/6/14
* @Created by 凉月-文
*/
public class DynamicDataSourceContextHolder {
//存放当前线程使用的数据源类型信息
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
//存放数据源id
public static List<String> dataSourceIds = new ArrayList<String>();
//设置数据源:提供给AOP去设置当前的线程的数据源的信息
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
//获取数据源: 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
public static String getDataSourceType() {
return contextHolder.get();
}
//清除数据源: 使用默认的数据源
public static void clearDataSourceType() {
contextHolder.remove();
}
//判断当前数据源是否存在
public static boolean isContainsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}
数据源的注解
设置数据源的注解,在具体的类上,或者在具体的方法上,dataSource是当前数据源的一个别名用于标识我们的数据源的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13import java.lang.annotation.*;
/**
* @Description 多数据源注解
* @Date 2021/6/14
* @Created by 凉月-文
*/
public DataSource {
String name() default "";
}
AOP拦截类
通过拦截上面的注解,在其执行之前处理, 设置当前执行SQL的数据源的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package tk.fulsun.demo.databases.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import tk.fulsun.demo.databases.DynamicDataSourceContextHolder;
import tk.fulsun.demo.databases.annotation.DataSource;
import java.lang.reflect.Method;
/**
* @Description 多数据源,切面处理类
* @Date 2021/6/14
* @Created by 凉月-文
*/
//@Order(1)
public class DataSourceAspect implements Ordered {
//@within在类上设置
//@annotation在方法上进行设置
public void dataSourcePointCut() {
}
public Object before(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//获取方法上的注解
DataSource ds = method.getAnnotation(DataSource.class);
if (ds == null || !DynamicDataSourceContextHolder.isContainsDataSource(ds.name())) {
//获取类上面的注解
ds = point.getTarget().getClass().getAnnotation(DataSource.class);
if (ds != null && DynamicDataSourceContextHolder.isContainsDataSource(ds.name())) {
DynamicDataSourceContextHolder.setDataSourceType(ds.name());
}else{
DynamicDataSourceContextHolder.setDataSourceType("master");
}
} else {
DynamicDataSourceContextHolder.setDataSourceType(ds.name());
}
log.info("AOP动态切换数据源,className:" + point.getTarget().getClass().getName() + ";methodName" + method.getName() + ";dataSourceKey:" + (StringUtils.isEmpty(ds.name()) ? "默认数据源" : ds.name()));
try {
return point.proceed();
} finally {
//清理掉当前设置的数据源,让默认的数据源不受影响
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
public int getOrder() {
return 1;
}
}
注册动态数据源
初始化数据源和提供了执行动态切换数据源的工具类
实现该ImportBeanDefinitionRegistrar接口的类拥有注册bean的能力。EnvironmentAware(获取配置文件配置的属性值)
使用@Import,如果括号中的类是ImportBeanDefinitionRegistrar的实现类,则会调用接口方法,将其中要注册的类注册成bean。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105package tk.fulsun.demo.databases;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* @Description 注册动态数据源
* @Date 2021/6/14
* @Created by 凉月-文
*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar , EnvironmentAware {
private static final String DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";
//默认数据源
private DataSource defaultDataSource;
//用户自定义数据源
private Map<String, DataSource> slaveDataSources = new HashMap<>();
public void setEnvironment(Environment environment) {
initDefaultDataSource(environment);
initslaveDataSources(environment);
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//添加默认数据源
targetDataSources.put("dataSource", this.defaultDataSource);
DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
//添加其他数据源
targetDataSources.putAll(slaveDataSources);
for (String key : slaveDataSources.keySet()) {
DynamicDataSourceContextHolder.dataSourceIds.add(key);
}
//创建DynamicDataSource
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
mpv.addPropertyValue("targetDataSources", targetDataSources);
//注册 - BeanDefinitionRegistry
registry.registerBeanDefinition("dataSource", beanDefinition);
log.info("Dynamic DataSource Registry");
}
private void initDefaultDataSource(Environment env) {
// 读取主数据源
Map<String, Object> dsMap = new HashMap<>();
String master = "spring.datasource.druid.master";
dsMap.put("driver", env.getProperty(master + ".driver-class-name"));
dsMap.put("url", env.getProperty(master + ".url"));
dsMap.put("username", env.getProperty(master + ".username"));
dsMap.put("password", env.getProperty(master + ".password"));
defaultDataSource = buildDataSource(dsMap);
}
private void initslaveDataSources(Environment env) {
Map<String, Object> dsMap = new HashMap<>();
String slave = "spring.datasource.druid.slave";
dsMap.put("driver", env.getProperty(slave + ".driver-class-name"));
dsMap.put("url", env.getProperty(slave + ".url"));
dsMap.put("username", env.getProperty(slave + ".username"));
dsMap.put("password", env.getProperty(slave + ".password"));
DataSource ds = buildDataSource(dsMap);
slaveDataSources.put("slave", ds);
}
public DataSource buildDataSource(Map<String, Object> dataSourceMap) {
try {
Object type = dataSourceMap.get("type");
if (type == null) {
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
}
Class<? extends DataSource> dataSourceType;
dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
String driverClassName = dataSourceMap.get("driver").toString();
String url = dataSourceMap.get("url").toString();
String username = dataSourceMap.get("username").toString();
String password = dataSourceMap.get("password").toString();
// 自定义DataSource配置
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
.username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package tk.fulsun.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import tk.fulsun.demo.databases.DynamicDataSourceRegister;
/**
* @Description 主启动类
* @Date 2021/6/14
* @Created by 凉月-文
*/
public class DynamicDataSourceApplication{
public static void main(String[] args) {
SpringApplication.run(DynamicDataSourceApplication.class, args);
}
}
测试
- 使用Mybatis逆向工程生成对应的mapper文件接口和实体类
Service类
service接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14public interface UserService {
int deleteByPrimaryKey(Integer id);
int insert(User record);
User selectByPrimaryKey(Integer id);
List<User> selectAll();
int updateByPrimaryKey(User record);
}实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class UserServiceImpl extends BaseService<User> implements UserService {
private UserMapper mapper;
public int deleteByPrimaryKey(Integer id) {
return mapper.deleteByPrimaryKey(id);
}
public int insert(User record) {
return mapper.insert(record);
}
public User selectByPrimaryKey(Integer id) {
return mapper.selectByPrimaryKey(id);
}
public List<User> selectAll() {
return mapper.selectAll();
}
public int updateByPrimaryKey(User record) {
return mapper.updateByPrimaryKey(record);
}
}
指定DataSource
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33package tk.fulsun.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.fulsun.demo.config.DataSourceNames;
import tk.fulsun.demo.databases.annotation.DataSource;
import tk.fulsun.demo.entity.User;
/**
* 测试多数据源
*/
public class DataSourceTestService {
private UserService userService;
public User queryUser(Integer userId) {
return userService.selectByPrimaryKey(userId);
}
public User queryUser2(Integer userId) {
return userService.selectByPrimaryKey(userId);
}
public User queryUser3(Integer userId) {
return userService.selectByPrimaryKey(userId);
}
}
测试
测试代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UserServiceImplTest {
private DataSourceTestService dataSourceTestService;
public void queryUser() {
System.out.println(dataSourceTestService.queryUser(1));
}
public void queryUser2() {
System.out.println(dataSourceTestService.queryUser2(1));
}
public void queryUser3() {
System.out.println(dataSourceTestService.queryUser3(1));
}
}
AbstractRoutingDataSource
AbstractRoutingDataSource类结构,继承了AbstractDataSource,AbstractRoutingDataSource使用模板类的模式,在父类定义了determineCurrentLookupKey()虚拟方法,获取lookupkey对象;其子类必须实现该方法。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
*配置的数据源
*/
private Map<Object, Object> targetDataSources;
/**
*默认数据源
*/
private Object defaultTargetDataSource;
// ......
/**
* spring InitializingBean 实现方法,bean初始化时调用
*/
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = resolveSpecifiedLookupKey(key);
DataSource dataSource = resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
/**
*获取jdbc链接时,调用determineTargetDataSource,获取指定的数据
*/
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
/**
*determineCurrentLookupKey方法通过子类自定义实现,获取lookupKey,然后从resolvedDefaultDataSource map对象中获取数据源
*/
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
/**
*子类必须实现的获取lookupKey的方法
*/
protected abstract Object determineCurrentLookupKey();
}- 分析AbstractRoutingDataSource可知,defaultTargetDataSource,表示默认的数据源;targetDataSources表示配置的所有数据源集合;
- afterPropertiesSet方法spring bean对象初始化方法,会把targetDataSources和defaultTargetDataSource,设置为resolvedDataSources和resolvedDefaultDataSource。
- getConnection()获取jdbc的连接,并通过determineTargetDataSource()获取指定的数据源
- determineTargetDataSource用到了我们需要进行实现的抽象方法determineCurrentLookupKey(),该方法返回需要使用的DataSource的key值,然后根据这个key从resolvedDataSources这个map里取出对应的DataSource,如果找不到,则用默认的resolvedDefaultDataSource。