SpringBoot多租户业务的多数据源动态切换解决方案

数据源切换方法

Springboot提供了AbstractRoutingDataSource抽象类,类名意思是数据源路由,让用户可以选择根据需要切换当前数据源

该类提供了一个抽象方法determineCurrentLookupKey(), 切换数据源时springboot会调用这个方法,所以只需要实现该方法,在该方法中返回需要切换的数据源名称即可

源码解读

1.从类关系图中可以看出AbstractRoutingDataSource类实现的是DataSource接口(非最底层),
其要求实现一个方法getConnection(),即获取DB连接

image.png

2.AbstractRoutingDataSource实现了这两个方法

image.png

其中determineTargetDataSource()调用determineCurrentLookupKey()方法,取到当前设定的查找键,通过查找键在上下文this.resolvedDataSources属性中尝试获取DataSource对象,这个对象即当前连接的数据源

image.png

3.那么this.resolvedDataSources在哪里维护?
AbstractRoutingDataSource类实现了InitializingBean类的afterPropertiesSet()方法,
在bean的所有属性设置完成后变会调用此方法,可以看到this.resolvedDataSources从this.targetDataSources取的信息;

image.png

所以只需要改变this.targetDataSources,并且触发afterPropertiesSet(),即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源

多租户业务背景

多租户业务场景下,往往每个租户都独立一个数据库(是否独立数据源实例根据实际需要处理),每个租户的数据在数据库层面先做了隔离,在开展详细业务编写时就可以不用考虑不同租户的数据会混淆。但是随之而来的就是数据源灵活切换的需求,需要封装一套方法,在业务编写时可以根据提供的租户代码便捷的切换到对应的数据源

提供的切换方式

1.注解方式切换

提供一个注解,可以根据租户代码切换,也可以根据配置文件中写定的数据源名称切换

2.直接调用方法方式切换

提供一个租户rds切换类,在编写业务代码时调用方法切换,该方式可以让租户代码以变量形式传递,无需提前知道

实现步骤概要

1.添加pom依赖、配置数据源信息

2.编写数据源配置类,将数据源配置信息注入到容器

3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法

4.编写租户rds切换类,业务切换数据源时统一调用此类

5.编写自定义注解

6.编写切面类,将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源

7.异常类、异常枚举类,规范异常抛出

详细步骤

1.pom依赖添加、配置数据源信息

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<dependencies>
<!-- mysql ps:由于连接的数据库是5.6所以用较老的包,读者可以根据数据库版本选择 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- druid数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>

application.yml

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
yaml复制代码# 主配置
spring:
# 数据源配置
datasource:
# 修改数据源为druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
# druid配置
druid:
# 主数据源
master:
driver-class-name: com.mysql.jdbc.Driver
# 默认数据库连接(配置库)
url: jdbc:mysql://xxx:xxx/config?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
# 递增db配置
db1:
driver-class-name: com.mysql.jdbc.Driver #这个要根据mysql-connector-java版本
url: jdbc:mysql://xxx:xxx/mydb?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: xxx
password: xxx
initial-size: 5 # 初始化时建立物理连接的个数
max-active: 30 # 最大连接池数量
min-idle: 5 # 最小连接池数量
max-wait: 60000 # 获取连接时最大等待时间,单位毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 # 连接保持空闲而不被驱逐的最小时间
validation-query: SELECT 1 FROM DUAL # 用来检测连接是否有效的sql,要求是一个查询语句
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
pool-prepared-statements: true # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: 50 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
filters: stat,wall,log4j2 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计;配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
use-global-data-source-stat: true # 合并多个DruidDataSource的监控数据
stat-view-servlet:
allow: '' # IP白名单(没有配置或者为空,则允许所有访问) allow: 127.0.0.1,192.168.163.1
deny: '' # IP黑名单 (存在共同时,deny优先于allow)
login-password: xxxxxx # 登录密码
login-username: admin # 登录名
reset-enable: false # 禁用HTML页面上的“Reset All”功能
url-pattern: /druid/* # 配置DruidStatViewServlet
web-stat-filter: # 配置DruidStatFilter
enabled: true
exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
url-pattern: /*

1.配置里面包含了一些druid的配置,可以根据业务需要自行配置

2.其中,spring.datasource.druid.master为主数据源,也是配置库数据源,租户库数据源连接信息会在配置库中获取,spring.datasource.druid.db1为递增数据源,db1可以命名为具体的业务库名称,这里仅仅方便理解取名为db1


2.编写数据源配置类,将数据源配置信息注入到容器

数据源配置类DataSourceConfig

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
java复制代码@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用
public class DataSourceConfig {
/**
* 默认数据源
*
* @return
*/
@Bean(DataSourceConstant.DATA_SOURCE_MASTER)
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource dataSourceMaster() {
return DruidDataSourceBuilder.create().build();
}

/**
* 递增数据源
*
* @return
*/
@Bean(DataSourceConstant.DATA_SOURCE_DB_1)
@ConfigurationProperties("spring.datasource.druid.db1")
public DataSource dataSourceDb1() {
return DruidDataSourceBuilder.create().build();
}


/**
* 设置动态数据源为主数据源
*
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource() {
// 将数据源设置进map
DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_MASTER, dataSourceMaster());
DynamicDataSource.setDataSourceMap(DataSourceConstant.DATA_SOURCE_DB_1, dataSourceDb1());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上
dynamicDataSource.setTargetDataSources(DynamicDataSource.dataSourceMap);
return dynamicDataSource;
}
}

数据源常量类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class DataSourceConstant {
private DataSourceConstant() {
}

/**
* 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
* 默认数据源名称
*/
public static final String DATA_SOURCE_MASTER = "dataSourceMaster";

/**
* 递增可配数据源名称
* 这里的命名统一在配置文件命名的基础上加dataSource前缀且改小驼峰
* 后面可接着 db2... dbn 也可以根据
*/
public static final String DATA_SOURCE_DB_1 = "dataSourceDb1";
}

此处先往DynamicDataSource.dataSourceMap将两个配置好的数据源连接信息写入,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上


3.编写DynamicDataSource类继承AbstractRoutingDataSource抽象类,维护当前数据源信息,提供切换方法

动态数据源类DynamicDataSource

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
java复制代码public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 存储当前线程的数据源key
*/
private static final ThreadLocal<String> DATA_SOURCE_KEY = ThreadLocal.withInitial(() -> DataSourceConstant.DATA_SOURCE_MASTER);

/**
* 数据源map
*/
public static Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>(1000);

/**
* 获取数据源key
*
* @return
*/
public static String getDataSourceKey() {
return DynamicDataSource.DATA_SOURCE_KEY.get();
}

/**
* 设置数据源key
*
* @param key
*/
public static void setDataSourceKey(String key) {
DynamicDataSource.DATA_SOURCE_KEY.set(key);
}

/**
* 移除默认数据源key
*/
public static void remove() {
DynamicDataSource.DATA_SOURCE_KEY.remove();
}

/**
* 切换成默认的数据源
*/
public static void setDataSourceDefault() {
setDataSource(DataSourceConstant.DATA_SOURCE_MASTER);
}

/**
* 切换成指定数据源 前提是dataSourceMap中有该key
* 外层调用时需要判断下map是否有,可靠性交给外层维护
*
* @param dataSource
*/
public static void setDataSource(String dataSource) {
setDataSourceKey(dataSource);
// InitializingBean.afterPropertiesSet()是,实例化后,bean的所有属性初始化后调用;但是如果该bean是直接从容器中拿的,并不需要实例化动作
// 这里直接拿到dataSource,手动触发一下,让AbstractRoutingDataSource.resolvedDataSources重新赋值,取到本类维护的map的值
DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringContextUtil.getBean("dataSource");
dynamicDataSource.afterPropertiesSet();
}

/**
* 获取租户数据源配置
*
* @param tenantCode
* @return
*/
public static Object getDataSourceMap(String tenantCode) {
return DynamicDataSource.dataSourceMap.get(tenantCode);
}

/**
* 设置map
*
* @param dataSourceName
* @return void
* @author Linzs
* @date 2021/8/28 11:53
**/
public static void setDataSourceMap(String dataSourceName, Object dataSource) {
dataSourceMap.put(dataSourceName, dataSource);
}

/**
* 设置map
*
* @param dataSourceName
* @return void
* @author Linzs
* @date 2021/8/28 11:53
**/
public static void setDataSourceMap(String dataSourceName) {
dataSourceMap.put(dataSourceName, SpringContextUtil.getBean(dataSourceName));
}

/**
* 设置租户数据源配置
*
* @param rdsConfig
* @return
*/
public static void setDataSourceMap(RdsConfig rdsConfig) {
DynamicDataSource.dataSourceMap.put(rdsConfig.getTenantCode(), getDruidDataSource(rdsConfig));
}

/**
* 获取DruidDataSource
*
* @param rdsConfig
* @return
*/
private static DruidDataSource getDruidDataSource(RdsConfig rdsConfig) {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://" + rdsConfig.getDbUrl() + ":" + rdsConfig.getDbPort() + "/" + rdsConfig.getDbName() + "?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=true&autoReconnect=true&serverTimezone=Asia/Shanghai");
druidDataSource.setUsername(rdsConfig.getDbAccount());
druidDataSource.setPassword(rdsConfig.getDbPassword());
return druidDataSource;
}

/**
* 重写determineCurrentLookupKey方法
*
* @return java.lang.Object
* @date 2021/8/28 12:14
**/
@Override
protected Object determineCurrentLookupKey() {
return getDataSourceKey();
}
}

  1. 维护了一个key,用于表示当前用的是哪个数据源
  2. 维护了一个map,用于springboot获取数据源信息
  3. 重写determineCurrentLookupKey方法,可参照上面源码解读理解

4.编写租户rds切换类,业务切换数据源时统一调用此类

RdsConfig类,该javabean描述rds的连接信息

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
java复制代码@Data
public class RdsConfig implements Serializable {

private static final long serialVersionUID = 1L;

/**
* 租户编码
*/
private String tenantCode;

/**
* 数据库URL
*/
private String dbUrl;

/**
* 数据库端口
*/
private String dbPort;

/**
* 数据库名称
*/
private String dbName;

/**
* 数据库账号
*/
private String dbAccount;

/**
* 数据库密码
*/
private String dbPassword;
}

具体rds切换服务类:TenantRdsServiceImpl类,实现TenantRdsService接口,这里不贴出来了

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
java复制代码@Service
@Slf4j
public class TenantRdsServiceImpl implements TenantRdsService {
@Autowired
private TenantMapper tenantMapper;

@Autowired
private RdsMapper rdsMapper;

/**
* 获取rds配置
*
* @param tenantCode
* @date 2021/8/28 13:53
**/
@Override
public RdsConfig getRdsConfig(String tenantCode) {
// 根据租户代码取租户表
Tenant tenant = tenantMapper.selectByTenantCode(tenantCode);
if (null == tenant) {
return null;
}
// 取rds表
Rds rds = rdsMapper.selectByPrimaryKey(tenant.getRdsId());
if (null == rds) {
return null;
}
// 转换为rds配置
RdsConfig rdsConfig = new RdsConfig();
rdsConfig.setDbUrl(rds.getHost());
rdsConfig.setTenantCode(tenantCode);
rdsConfig.setDbName(tenant.getDbName());
rdsConfig.setDbAccount(rds.getAccount());
rdsConfig.setDbPassword(rds.getPwd());
rdsConfig.setDbPort(String.valueOf(rds.getPort()));
return rdsConfig;
}

/**
* 根据租户代码切换rds连接,同一个线程内rds配置只会查一次
*
* @param tenantCode
* @date 2021/8/28 13:16
**/
@Override
public void switchRds(String tenantCode) {
if (StringUtils.isBlank(tenantCode)) {
throw new TenantCodeIsBlankException();
}
// 如果当前已是这个租户rds则直接返回
if (tenantCode.equals(DynamicDataSource.getDataSourceKey())) {
return;
}
// 如果本地已有则不查了 改rds需要重启服务
if (null == DynamicDataSource.getDataSourceMap(tenantCode)) {
// 如果当前不是配置库则先切回配置库
if (!DataSourceConstant.DATA_SOURCE_MASTER.equals(DynamicDataSource.getDataSourceKey())) {
DynamicDataSource.setDataSourceDefault();
}
// 获取rds配置
RdsConfig rdsConfig = getRdsConfig(tenantCode);
if (null == rdsConfig) {
throw new RdsNotFoundException();
}
DynamicDataSource.setDataSourceMap(rdsConfig);
}
// 切换到业务库
DynamicDataSource.setDataSource(tenantCode);
}

/**
* 根据数据源名称切换rds连接,同一个线程内rds配置只会查一次
*
* @param dataSourceName
* @date 2021/8/28 13:16
**/
@Override
public void switchRdsByDataSourceName(String dataSourceName) {
if (StringUtils.isBlank(dataSourceName)) {
throw new DataSourceNameIsEmptyException();
}
// 如果当前已是这个数据源直接返回
if (dataSourceName.equals(DynamicDataSource.getDataSourceKey())) {
return;
}
// 如果本地已有则不查了 改rds需要重启服务
if (null == DynamicDataSource.getDataSourceMap(dataSourceName)) {
throw new DataSourceNotExistException();
}
// 切换
DynamicDataSource.setDataSource(dataSourceName);
}
}

1.这里用到了两张表,一张是租户表(tenant)用于存储租户代码与rds的对应关系,另一张是DB连接信息(rds)表,用于存储数据源连接信息,具体的mapper和javabean的代码这里就不贴出来了,根据需求建表具体实现即可

2.提供了三个方法分别是根据租户代码获取rds连接信息,根据租户代码切换rds,根据数据源名称切换rds,切换方法中对当前连接信息做了判断,不会重复切换,也不会重复查配置库获取rds信息


5.编写自定义注解

自定义注解 @SwitchMasterRds

1
2
3
4
5
6
7
8
java复制代码/**
* 切换至主数据源-自定义注解
* 这个仅为了方便使用,用SwitchRds注解指定为默认数据源也可以实现
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchMasterRds {
}

自定义注解 @SwitchRds

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码/**
* 切换数据源-自定义注解
*/
// 注解作用目标;ElementType.METHOD表示该注解会用在方法上;ElementType.TYPE表示该注解会用在类,接口,枚举;
@Target({ElementType.METHOD, ElementType.TYPE})
// 注解策略属性;RetentionPolicy.RUNTIME表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchRds {
/**
* 根据数据源bean切换数据源
* 此处可以切换的数据源在 DataSourceConfig 配置类中
* 同时指定了tenantCode则这个优先
*/
String dataSource() default "";

/**
* 动态切换-根据租户代码切换数据源
*/
String tenantCode() default "";
}

1.SwitchRds注解既可以用租户代码切换rds,又可以使用数据源名称切换

2.SwitchMasterRds注解是为了方便切换成主数据源而添加的


6.编写切面类

SwitchMasterRds注解的切面类SwitchMasterRdsAspect

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
java复制代码@Aspect
@Component
@Slf4j
public class SwitchMasterRdsAspect {
/**
* 租户rds服务类
*/
@Autowired
private TenantRdsService tenantRdsServiceImpl;

/**
* 切点
* 连接点:直接指定为注解
* 注意:com.xxx.SwitchMasterRds这里包名自行修改
* @date 2021/8/27 14:26
**/
@Pointcut("@annotation(com.xxx.SwitchMasterRds)")
public void myPointcut() {
}

/**
* 环绕通知
*
* @return java.lang.Object
* @date 2021/8/27 14:26
**/
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object proceed;
try {
tenantRdsServiceImpl.switchRdsByDataSourceName(DataSourceConstant.DATA_SOURCE_MASTER);
// 执行
proceed = pjp.proceed();
} finally {
// todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
}
return proceed;
}
}

SwitchRds注解的切面类SwitchRdsAspect

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
java复制代码@Aspect
@Component
@Slf4j
public class SwitchRdsAspect {
/**
* 租户rds服务类
*/
@Autowired
private TenantRdsService tenantRdsServiceImpl;

/**
* 切点
* 连接点:直接指定为注解
* 注意:com.xxx.SwitchRds这里包名自行修改
* @date 2021/8/27 14:26
**/
@Pointcut("@annotation(com.xxx.SwitchRds)")
public void myPointcut() {
}

/**
* 环绕通知
*
* @return java.lang.Object
* @date 2021/8/27 14:26
**/
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
SwitchRds annotation = getAnnotation(pjp);
// 获取注解上的租户代码
String tenantCode = annotation.tenantCode();
String dataSource = annotation.dataSource();
Object proceed;
try {
if (StringUtils.isNotBlank(dataSource)) {
tenantRdsServiceImpl.switchRdsByDataSourceName(dataSource);
} else if (StringUtils.isNotBlank(tenantCode)) {
tenantRdsServiceImpl.switchRds(tenantCode);
} else {
throw new DataSourceSwitchFailException();
}
// 执行
proceed = pjp.proceed();
} finally {
// todo 这里需要做移除切换的数据源也可以,但是如果没移除再下次切换的时候会先切换到配置库
}
return proceed;
}

/**
* 获取注解
*
* @param pjp
* @date 2021/8/27 17:58
**/
private SwitchRds getAnnotation(ProceedingJoinPoint pjp) {
// 尝试获取类上的注解
SwitchRds annotation = pjp.getTarget().getClass().getAnnotation(SwitchRds.class);
// 如果类上没有注解则获取方法上面的
if (null == annotation) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
annotation = methodSignature.getMethod().getAnnotation(SwitchRds.class);
}
return annotation;
}

}

这里将连接点直接设定在编写的自定义注解上,根据参数等调用rds切换类切换数据源


7.异常类、异常枚举类

ErrorInfo接口,规范异常枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface ErrorInfo {
/**
* 异常码
* @return int
*/
int code();

/**
* 异常描述
* @return String
*/
String message();
}

处理异常枚举类,将所有错误类型以及错误代码枚举出来

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
105
java复制代码/**
* 处理异常枚举类
*/
public enum HandleExceptionEnum implements ErrorInfo {
/**
* 待处理
*/
WAIT(0, "待处理"),

/**
* 成功
*/
SUCCESS(10, "SUCCESS"),

/**
* 程序错误
*/
ERROR(100, "程序错误"),


/**
* 公共 - rds配置未取到
*/
C_GENERATE_RDS_NOT_FOUND(1001, "rds配置未取到"),

/**
* 公共 - 租户代码为空
*/
C_GENERATE_TENANT_CODE_IS_BLANK(1002, "租户代码为空"),

/**
* 公共 - 数据源配置不存在
*/
C_GENERATE_DATA_SOURCE_NOT_EXIST(1003, "数据源配置不存在"),

/**
* 公共 - 数据源名称为空
*/
C_GENERATE_DATA_SOURCE_NAME_IS_EMPTY(1004, "数据源名称为空"),

/**
* 公共 - 数据源名称为空
*/
C_GENERATE_DATA_SOURCE_SWITCH_FAIL(1005, "数据源切换失败"),


// ------------------------------------------------------------------

;

/**
* 编码
*/
private final int code;

/**
* 信息
*/
private final String message;

HandleExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}

@Override
public int code() {
return code;
}

@Override
public String message() {
return message;
}

/**
* code转换成enum
*
* @param code 错误码
* @return HandleExceptionEnum
*/
public static HandleExceptionEnum codeOf(int code) {
for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
if (item.code() == code) {
return item;
}
}
return null;
}

/**
* 指定code是否在枚举之内
*
* @param code 错误码
* @return boolean
*/
public static boolean contain(int code) {
for (HandleExceptionEnum item : HandleExceptionEnum.values()) {
if (item.code() == code) {
return true;
}
}
return false;
}
}

处理异常基类,所有的处理异常全部继承该类,其保存ErrorInfo信息,可以方便获取错误代码等

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
java复制代码/**
* HandlerException
*/
public class HandlerException extends RuntimeException {
/**
* 异常信息
*/
private final ErrorInfo errorInfo;

/**
* 无参构造方法默认为程序错误
*/
public HandlerException() {
super(HandleExceptionEnum.ERROR.message());
this.errorInfo = HandleExceptionEnum.ERROR;
}

public HandlerException(HandleExceptionEnum handleExceptionEnum) {
super(handleExceptionEnum.message());
this.errorInfo = handleExceptionEnum;
}

public HandlerException(HandleExceptionEnum handleExceptionEnum, String message) {
super(message);
this.errorInfo = handleExceptionEnum;
}

/**
* 根据异常类型获取code
*
* @param e
* @return int
*/
public static int getCode(Exception e){
return e instanceof HandlerException ? ((HandlerException) e).getErrorInfo().code() : HandleExceptionEnum.ERROR.code();
}

/**
* 获取异常信息
*
* @return ErrorInfo
*/
public ErrorInfo getErrorInfo() {
return errorInfo;
}
}

具体的异常类,直接继承处理异常基类,文中主动抛出的异常全是这样方式编写,这里就不一一列举了

1
2
3
4
5
6
7
8
java复制代码/**
* rds配置未取到
*/
public class RdsNotFoundException extends HandlerException {
public RdsNotFoundException() {
super(HandleExceptionEnum.C_GENERATE_RDS_NOT_FOUND);
}
}

使用

1.注解方式

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
java复制代码@RestController
public class HelloController {
/**
* 切换到主数据源方式1
*/
@GetMapping("/masterFirst")
@SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_MASTER)
public Object masterFirst() {
// todo
}

/**
* 切换到主数据源方式2
*/
@GetMapping("/masterSecond")
@SwitchMasterRds
public Object masterSecond() {
// todo
}

/**
* 切换到其他已配置的数据源
*/
@GetMapping("/other")
@SwitchRds(dataSource = DataSourceConstant.DATA_SOURCE_DB_1)
public Object other() {
// todo
}

/**
* 根据租户代码切换
*/
@GetMapping("/tenant")
@SwitchRds(tenantCode = "tenantxxx")
public Object tenant() {
// todo
}
}

2.业务代码使用方式

1
2
3
4
5
java复制代码try {
tenantRdsServiceImpl.switchRds("any tenant code");
} catch (Exception e) {
log.error("切换租户rds时出错:{},context:{}", e.getMessage(), context, e);
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%