spring-boot 集成多租户(SaaS)

一、使用场景

​ 本次集成是针对单/多数据源,但是不是动态数据源的 Java 后端项目;我这次更改的需求是将一个多模块的项目更改为动态的多数据源,并且新增切换数据源的 AOP,让其能够在使用注解的方式自动切换数据源。同时业务的数据库需要跟随租户的新增而新增。

二、使用组件

​ 主要使用 dynamic-datasource 的多数据源仓库。

三、具体方法

1、配置 pom 文件

1
2
3
4
5
6
<!--多租户-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>

这里我使用的版本是 3.5.1,据说 3.2版本一下支持比较少

2、配置 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
#服务器端口号
server:
port: 6666
servlet:
context-path: /text

#通用配置
spring:
#当前激活的配置文件
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
#数据源的基本信息
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: root
loginPassword: 111111
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://ip-port
username: root
password: 111111
driver-class-name: com.mysql.jdbc.Driver
slave:
url: jdbc:mysql://ip-port
username: root
password: 111111
driver-class-name: com.mysql.jdbc.Driver
这个文件中,我最主要的配置属性就是 spring.datasource ,这是数据源的配置

1)其中 spring.datasource.druid 是因为我的项目集成了 Druid,使用 dynamic-datasource的话需要集成,配置就按照我这样默认配置就行

2)spring.datasource.druid.dynamic 这里的配置属性就在于 primary 设置主数据源了,向我这里是有两个数据库,一个是 master 的用户数据库,一个是 slave 的业务数据库,我把 master 设为主数据库是因为用户这个书库我还是把它认定为静态数据库,不会跟随动态数据库的数据源的新增而新增一个新的业务数据库。

注意:由于我是两个模块,所以这个配置文件新增的数据源配置需要在两个模块的 application.yml 文件中都进行添加。

3、AOP 切面代码

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DynamicDataSourChange {
//dataSource 自定义注解的参数
String dataSource() default DataSourceType.DATA_SOURCE_SYS;
}
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
/*
* 数据源的名称常量,用于切换数据源的参数设置
*/
public class DataSourceType {

// master 以及 slave 名称必须和数据源相同
public static final String DATA_SOURCE_MASTER = "master";
public static final String DATA_SOURCE_SLAVE = "slave";


// 设置线程
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

// 设置数据源类型
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
// 获取数据源类型
public static String getDbType() {
return contextHolder.get();
}
// 清除数据源类型
public static void clearDbType() {
contextHolder.remove();
}
}
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
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {

@SuppressWarnings("rawtypes")
@Before("@annotation(com.test.DynamicDataSourChange)") //前置通知
public void before(JoinPoint point){
//获得当前访问的class
Class<?> className = point.getTarget().getClass();
DynamicDataSourChange dataSourceAnnotation = className.getAnnotation(DynamicDataSourChange.class);
//这个判断要求@DynamicDataSourChange必须在类上注解,并且还要在其方法中进行二次注解。
if (dataSourceAnnotation != null ) {
//获得访问的方法名
String methodName = point.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
String dataSource = DataSourceContextHolder.DATA_SOURCE_SYS;
try {
Method method = className.getMethod(methodName, argClass);
if (method.isAnnotationPresent(DynamicDataSourChange.class)) {
DynamicDataSourChange annotation = method.getAnnotation(DynamicDataSourChange.class);
dataSource = annotation.dataSource();
// 这里是切换数据库,我将切换数据库的关键 id(ChangeId) 存在了token 里面
if (dataSource.equals(DataSourceContextHolder.DATA_SOURCE_MASTER)) {
HttpServletRequest request = WebContext.getRequest();
dataSource = dataSource + "_" + request.getAttribute(ChangeId).toString();
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

DynamicDataSourceContextHolder.poll();
DynamicDataSourceContextHolder.push(dataSource);

}

}

@SuppressWarnings("rawtypes")
@After("@annotation(com.test.DynamicDataSourChange)") //后置通知
public void after(JoinPoint point){
//获得当前访问的class
Class<?> className = point.getTarget().getClass();
DynamicDataSourChange dataSourceAnnotation = className.getAnnotation(DynamicDataSourChange.class);
if (dataSourceAnnotation != null) {
// 获得访问的方法名
String methodName = point.getSignature().getName();
// 得到方法的参数的类型
Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
String dataSource = DataSourceContextHolder.DATA_SOURCE_MASTER;
try {
Method method = className.getMethod(methodName, argClass);
if (method.isAnnotationPresent(DynamicDataSourChange.class)) {
DynamicDataSourChange annotation = method.getAnnotation(DynamicDataSourChange.class);
dataSource = annotation.dataSource();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (dataSource != null && !DataSourceContextHolder.DATA_SOURCE_MASTER.equals(dataSource)) {
DynamicDataSourceContextHolder.clear();
}
}
}
}

四、使用方法

1、加注解,这边建议在 impl 层里面加注解

1
2
3
4
5
6
7
8
9
10
11
@Service()
@DynamicDataSourceAnnotation
public class SmartlockServiceImpl implements SmartlockService {

@Override
@DynamicDataSourceAnnotation(dataSource = DataSourceType.DATA_SOURCE_MASTER)
public List getList() throws Exception {

####### 业务代码 #######

}

2、手动切换数据源

1
2
DynamicDataSourceContextHolder.clear(); // 手动切换数据源
DynamicDataSourceContextHolder.push("slave");

spring-boot 集成多租户(SaaS)
https://tdsgpo.top/2023/11/15/spring-boot 集成多租户(SaaS)/
作者
DDS
发布于
2023年11月15日
许可协议