Skip to content

Commit ba1e2b7

Browse files
committed
1.1.0 Release
1 parent 2142cc9 commit ba1e2b7

File tree

8 files changed

+197
-60
lines changed

8 files changed

+197
-60
lines changed

README.md

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
| saas-datasource-spring-boot-starter | dynamic-datasource-spring-boot-starter | mybatis-plus-boot-starter | mybatis-spring-boot-starter |
4141
| :----: | :----: | :----: | :----: |
42-
| 1.0.0 | version <= 2.4.2 | <div align="left">根据`@SaaS`注解的位置分为两种情况:<br/>1. 如果注解在Mapper上,则 version <= 3.0.7.1,若高于此版本dynamic-datasource会报错;<br/>2. 如果注解不在Mapper上,则可使用目前最新版本 version <= 3.5.1 (latest)。<br/>按[最佳实践](#最佳实践),推荐上述第二种情况,注解不要放在Mapper上。</div> | version <= 2.2.2 (latest) |
42+
| 1.1.0 & 1.0.0 | version <= 2.4.2 | <div align="left">根据`@SaaS`注解的位置分为两种情况:<br/>1. 如果注解在Mapper上,则 version <= 3.0.7.1,若高于此版本dynamic-datasource会报错;<br/>2. 如果注解不在Mapper上,则可使用目前最新版本 version <= 3.5.1 (latest)。<br/>按[最佳实践](#最佳实践),推荐上述第二种情况,注解不要放在Mapper上。</div> | version <= 2.2.2 (latest) |
4343

4444
## 快速使用
4545

@@ -49,7 +49,7 @@
4949
<dependency>
5050
<groupId>com.air-software</groupId>
5151
<artifactId>saas-datasource-spring-boot-starter</artifactId>
52-
<version>1.0.0</version>
52+
<version>1.1.0</version>
5353
</dependency>
5454
```
5555

@@ -124,6 +124,23 @@ public class MySaaSDataSourceProvider implements SaaSDataSourceProvider {
124124
}
125125
```
126126

127+
**注意**,为保证数据源提供者查询数据源时能够正确访问到公共库,而不受其他已切换数据源的影响,建议为查询数据源的Mapper添加`dynamic-datasource-spring-boot-starter`自带的`@DS`注解,强制使用公共库:
128+
129+
```
130+
@DS("common")
131+
public interface DataSourceConfigMapper
132+
```
133+
134+
另外如果你使用的是`1.1.0`及以上版本,可以直接使用`SaaSDataSource.switchTo`来强制切换至公共库,记得切换后立即调用`clearCurrent`清理一下:
135+
136+
```
137+
SaaSDataSource.switchTo("common");
138+
DataSourceConfig dataSourceConfig = dataSourceConfigMapper.selectById(dsKey);
139+
SaaSDataSource.clearCurrent();
140+
```
141+
142+
不建议在`1.0.0`采用此方法,因为`1.0.0`版本的`SaaSDataSource.switchTo`方法并未被优化。
143+
127144
### 启用注解
128145

129146
在SpringBoot主启动类上添加`@EnableSaaSDataSource`注解,表示启用SaaS数据源功能。
@@ -139,12 +156,10 @@ public class SaaSApplication {
139156
}
140157
```
141158

142-
在需要切换数据源的类或方法上标记`@SaaS`注解,此注解中可设置**租户标识字段名称**,默认值为`dsKey`
159+
如果你想使用Request Session或Header的方式切换数据源,则在需要切换数据源的类或方法上标记`@SaaS`注解,此注解中可设置**租户标识字段名称**,默认值为`dsKey`
143160

144161
比如我在使用注解时设置为`@SaaS("tenantId")`,那么我在Request Session或Header中就需要用`tenantId`字段来设置租户标识,而这个租户标识在首次切换至此租户时,会传递至你自己实现的`SaaSDataSourceProvider`中,以此来获取租户对应数据源。
145162

146-
注意:如果使用了`SaaSDataSource`上下文来手动切换数据库,则`@SaaS`中的值会被忽略。
147-
148163
### 切换数据源
149164

150165
本工具共提供三种方式来切换数据源,按优先级从高到底排列如下:
@@ -155,13 +170,21 @@ public class SaaSApplication {
155170

156171
你可以在任意地方多次调用`SaaSDataSource.switchTo`来手动切换数据源,他会影响到你下一次即将执行的数据库操作,常用于拦截器、定时任务、异步操作、循环刷库,跨库统计、消息消费等场景。
157172

173+
`SaaSDataSource`内部模拟了一个栈来存储多次切换的数据源,`switchTo`方法入栈,`current`方法获取当前栈顶数据,`clearCurrent`方法会让当前栈顶数据出栈,`clearAll`方法会清理整个栈。
174+
175+
建议在每次手动切换完成,且对应数据源的业务处理完成后,调用`clearCurrent`来清理刚刚手动切换的数据源,以避免对后续流程产生影响。或者也可以在整个业务流程的最后调用`clearAll`
176+
177+
**注意**:在`1.1.0`及以上版本,如果你只使用`SaaSDataSource`来手动切换数据库,则不需要再标记`@SaaS`注解,即使标记了注解,注解中的值也会被忽略。
178+
179+
而在`1.0.0`版本,则仍需要标记`@SaaS`注解,切必须在注解生效前执行`SaaSDataSource.switchTo`
180+
158181
## 注意事项
159182

160183
以下注意事项**非常重要**,请开发者务必仔细阅读:
161184

162185
1. 公共库中的表最好不要跟租户库中的业务表有重合,因为当切换数据源失败时,会自动切换回应用启动时配置的默认数据源(通常为公共库),此时如果公共库中存在同名业务表的话,那在明面上是不会报错的,只不过数据都到公共库里了,这样不利于排查问题。
163186
2. 为安全起见,尽量不要使用Header模式,因为前端传递的数据永远是不可信的。如果要使用前端直接传递的值,一定要配合权限控制,比如整个系统的超级管理员想要自由切换至不同租户,此时就需要使用前端传值。这也是我保留了Header模式,但优先级降为最低的原因。
164-
3. **事务中无法切换数据源**首先一定要注意`@SaaS`的标记位置,至少应在最外层事务或更上一层的调用方标记此注解,即保证注解在事务开启前发挥作用,以切换到正确的数据源。其次不要在事务内调用`SaaSDataSource.switchTo`,而应在事务开启前调用。**如果没有在事务开启前通过注解或手动切换至正确的数据源,则事务会在默认数据源上执行。**
187+
3. **事务中无法切换数据源,强行切换可能会导致报错。**首先一定要注意`@SaaS`的标记位置,至少应在最外层事务或更上一层的调用方标记此注解,即保证注解在事务开启前发挥作用,以切换到正确的数据源。其次不要在事务内调用`SaaSDataSource.switchTo`,而应在事务开启前调用。**如果没有在事务开启前通过注解或手动切换至正确的数据源,则事务会在默认数据源上执行。**
165188
4. 本工具**不提供分布式事务的实现**,也未做过相关测试,如果需要分布式事务请开发者自行实现和测试,理论上本工具兼容分布式事务。
166189
5. 在定时任务、异步操作、消息消费等无法获取Request上下文的场景下,**一定要记得处理业务前调用`SaaSDataSource.switchTo`来手动切换数据源**
167190

@@ -170,4 +193,16 @@ public class SaaSApplication {
170193
基于上述注意事项,结合现代Web开发的技术倾向,可以得出以下几条最佳实践:
171194

172195
1. 为保障注解在事务开启前发挥作用,**在Web项目中推荐将`@SaaS`标记在`Controller`**,一般这就是事务的顶层了。大部分项目中都会有一个`BaseController`作为所有Controller的父类,将`@SaaS`注解标记在父类上,对所有Controller都会起作用。
173-
2. 现代Web项目中使用Token的情况已逐步超过Session,在Token场景下,我们可以将`dsKey`放入Token中,或为安全起见将`dsKey`放入Redis,而Redis Key放入Token中。随后我们在拦截器中解析Token之后,使用获得的`dsKey`调用`SaaSDataSource.switchTo`来切换数据源,这样在编写业务代码时就无需关心租户切换问题了。(不要忘了最后在拦截器的`afterCompletion`中调用`SaaSDataSource.clear`方法)
196+
2. 现代Web项目中使用Token的情况已逐步超过Session,在Token场景下,我们可以将`dsKey`放入Token中,或为安全起见将`dsKey`放入Redis,而Redis Key放入Token中。随后我们在拦截器中解析Token之后,使用获得的`dsKey`调用`SaaSDataSource.switchTo`来切换数据源,这样在编写业务代码时就无需关心租户切换问题了,最后不要忘了在拦截器的`afterCompletion`中调用`SaaSDataSource.clearAll`方法(`1.0.0`版本是`SaaSDataSource.clear`)。
197+
198+
## 更新日志
199+
200+
### 1.1.0
201+
202+
- 优化了`SaaSDataSource`,现在可以随时强制切换数据源,不再依赖`@SaaS`注解;
203+
- 增加了数据源管理器,优化内部拦截器代码。
204+
205+
### 1.0.0
206+
207+
- 支持Request Session和Header切换数据源;
208+
- 支持`SaaSDataSource`手动切换数据源,但需要在切换后的调用流程中存在`@SaaS`注解标记来触发。

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.air-software</groupId>
77
<artifactId>saas-datasource-spring-boot-starter</artifactId>
8-
<version>1.0.0</version>
8+
<version>1.1.0</version>
99
<packaging>jar</packaging>
1010

1111
<name>saas-datasource-spring-boot-starter</name>

src/main/java/com/airsoftware/saas/datasource/annotation/EnableSaaSDataSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import java.lang.annotation.*;
2222

2323
/**
24-
* 开启动态数据源
24+
* 开启动态数据源<br/>
2525
* 添加到SpringBootApplication启动类上
2626
*
2727
* @author bit

src/main/java/com/airsoftware/saas/datasource/annotation/SaaS.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
import java.lang.annotation.*;
1919

2020
/**
21-
* 标记需要开启SaaS模式的类或方法,也可以标记在父类上。
22-
*
23-
* 注意事项:
24-
* 1、由于事务内部无法切换数据源,因此如果需要使用事务,则应至少在最外层事务或更上一层的调用方标记此注解,即保证注解在事务开启前发挥作用,以切换到正确的数据源;
25-
* 2、本工具不提供分布式事务的实现,也未做过相关测试,如果需要分布式事务请开发者自行实现和测试,理论上本工具兼容分布式事务。
21+
* 标记需要开启SaaS模式的类或方法,也可以标记在父类上。<br/>
22+
* <br/>
23+
* 注意事项:<br/>
24+
* 1、由于事务内部无法切换数据源,因此如果需要使用事务,则应至少在最外层事务或更上一层的调用方标记此注解,即保证注解在事务开启前发挥作用,以切换到正确的数据源;<br/>
25+
* 2、本工具不提供分布式事务的实现,也未做过相关测试,如果需要分布式事务请开发者自行实现和测试,理论上本工具兼容分布式事务。<br/>
2626
*
2727
* @author bit
2828
*/
@@ -32,7 +32,7 @@
3232
public @interface SaaS {
3333

3434
/**
35-
* 租户标识字段名称,即Request Session或Header中对应的字段名称。
35+
* 租户标识字段名称,即Request Session或Header中对应的字段名称。<br/>
3636
* 如果使用 {@link com.airsoftware.saas.datasource.context.SaaSDataSource} 来手动切换数据源,则此值会被忽略。
3737
*/
3838
String value() default "dsKey";

src/main/java/com/airsoftware/saas/datasource/context/SaaSDataSource.java

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,103 @@
1515
*/
1616
package com.airsoftware.saas.datasource.context;
1717

18+
import com.airsoftware.saas.datasource.core.SaaSDataSourceManager;
19+
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
20+
import lombok.Setter;
21+
22+
import java.util.concurrent.LinkedBlockingDeque;
23+
1824
/**
19-
* SaaS数据源切换上下文
25+
* SaaS数据源手动切换工具<br/>
2026
* 用于手动切换数据源,可在同一调用流程内多次切换数据源,注意在开启了事务的方法内切换无效。
2127
*
2228
* @author bit
2329
*/
2430
public class SaaSDataSource {
2531

26-
private static ThreadLocal<String> HOLDER = ThreadLocal.withInitial(() -> "");
32+
@Setter
33+
private static SaaSDataSourceManager manager;
34+
35+
/**
36+
* 模拟栈存储手动切换的数据源<br/>
37+
* 在一次调用流程中,如果启用了手动切换,则后续只会以手动切换为准,除非显式调用clearCurrent或clearAll。<br/>
38+
* 如果一次调用流程中多次设置手动切换,则clearCurrent会移除栈顶数据,clearAll会清空整个栈。
39+
*/
40+
private static ThreadLocal<LinkedBlockingDeque<String>> DS_KEY_HOLDER = ThreadLocal.withInitial(LinkedBlockingDeque::new);
2741

2842
/**
29-
* 获取当前数据源标识
43+
* 获取当前栈顶的数据源标识
3044
*/
3145
public static String current() {
32-
return HOLDER.get();
46+
LinkedBlockingDeque<String> deque = DS_KEY_HOLDER.get();
47+
return deque.isEmpty() ? null : deque.getFirst();
3348
}
3449

3550
/**
3651
* 切换至对应数据源
3752
* @param dsKey 数据源标识
3853
*/
3954
public static void switchTo(String dsKey) {
40-
HOLDER.set(dsKey);
55+
set(dsKey);
4156
}
4257

4358
/**
4459
* 切换至对应数据源
4560
* @param dsKey 数据源标识,兼容Long型ID
4661
*/
4762
public static void switchTo(Long dsKey) {
48-
HOLDER.set(String.valueOf(dsKey));
63+
set(String.valueOf(dsKey));
64+
}
65+
66+
/**
67+
* 切换至对应数据源
68+
* @param dsKey 数据源标识,兼容Integer型ID
69+
*/
70+
public static void switchTo(Integer dsKey) {
71+
set(String.valueOf(dsKey));
72+
}
73+
74+
/**
75+
* 添加数据源,并手动设置上下文
76+
* @param dsKey
77+
*/
78+
private static void set(String dsKey) {
79+
// 设置时即添加数据源
80+
manager.addDataSource(dsKey);
81+
// 入栈
82+
DS_KEY_HOLDER.get().addFirst(dsKey);
83+
// 设置DynamicDataSource上下文,实际数据库操作时以此为准,其内部也是一个栈
84+
DynamicDataSourceContextHolder.setDataSourceLookupKey(dsKey);
85+
}
86+
87+
/**
88+
* 移除当前数据源<br/>
89+
* 如果当前线程是连续手动切换数据源,只会移除掉当前栈顶生效的数据源。
90+
*/
91+
public static void clearCurrent() {
92+
LinkedBlockingDeque<String> deque = DS_KEY_HOLDER.get();
93+
if (deque.isEmpty()) {
94+
DS_KEY_HOLDER.remove();
95+
} else {
96+
deque.pollFirst();
97+
}
98+
DynamicDataSourceContextHolder.clearDataSourceLookupKey();
4999
}
50100

51101
/**
52-
* 清空线程
102+
* 清空所有手动设置的数据源<br/>
103+
* 如果当前线程是连续手动切换数据源,则会清空整个栈。
53104
*/
54-
public static void clear() {
55-
HOLDER.remove();
105+
public static void clearAll() {
106+
LinkedBlockingDeque<String> deque = DS_KEY_HOLDER.get();
107+
// 根据本工具当前栈的深度,将DynamicDataSource上下文中手动设置进去的数据源依次出栈
108+
int size = deque.size();
109+
for (int i = 0; i < size; i++) {
110+
DynamicDataSourceContextHolder.clearDataSourceLookupKey();
111+
}
112+
// 清空本工具的栈,并移除线程
113+
deque.clear();
114+
DS_KEY_HOLDER.remove();
56115
}
57116

58117
}

src/main/java/com/airsoftware/saas/datasource/core/SaaSDataSourceAnnotationInterceptor.java

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717

1818
import com.airsoftware.saas.datasource.annotation.SaaS;
1919
import com.airsoftware.saas.datasource.context.SaaSDataSource;
20-
import com.airsoftware.saas.datasource.provider.SaaSDataSourceProvider;
2120
import com.airsoftware.saas.datasource.util.StringUtil;
2221
import com.baomidou.dynamic.datasource.DynamicDataSourceClassResolver;
23-
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
2422
import com.baomidou.dynamic.datasource.spel.DynamicDataSourceSpelParser;
2523
import com.baomidou.dynamic.datasource.spel.DynamicDataSourceSpelResolver;
2624
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
@@ -34,9 +32,7 @@
3432
import org.springframework.web.context.request.ServletRequestAttributes;
3533

3634
import javax.servlet.http.HttpServletRequest;
37-
import javax.sql.DataSource;
3835
import java.lang.reflect.Method;
39-
import java.util.Map;
4036

4137
/**
4238
* SaaS数据源AOP核心拦截器
@@ -47,10 +43,7 @@
4743
public class SaaSDataSourceAnnotationInterceptor implements MethodInterceptor {
4844

4945
@Setter
50-
private SaaSDataSourceProvider dynamicDataSourceProvider;
51-
52-
@Setter
53-
private DynamicRoutingDataSource dynamicRoutingDataSource;
46+
private SaaSDataSourceManager manager;
5447

5548
@Setter
5649
private DynamicDataSourceSpelResolver dynamicDataSourceSpelResolver;
@@ -62,9 +55,10 @@ public class SaaSDataSourceAnnotationInterceptor implements MethodInterceptor {
6255

6356
@Override
6457
public Object invoke(MethodInvocation invocation) throws Throwable {
58+
String dsKey = "";
59+
boolean requestAddDataSource = false;
6560
try {
6661
String dsKeyField = getDsKeyField(invocation);
67-
String dsKey = "";
6862

6963
// Request优先级:Session > Header
7064
if (RequestContextHolder.getRequestAttributes() != null) {
@@ -73,24 +67,26 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
7367
dsKey = StringUtil.isNotBlank(sessionValue) ? sessionValue : request.getHeader(dsKeyField);
7468
}
7569

76-
// 手动设置数据源优先级最高
70+
// SaaSDataSource手动设置数据源优先级最高,因此如果SaaSDataSource当前栈顶有值,则忽略Request设置值,否则按Request设置值切换上下文
7771
String currentContext = SaaSDataSource.current();
78-
if (StringUtil.isNotBlank(currentContext)) {
79-
dsKey = currentContext;
80-
}
81-
82-
if (StringUtil.isNotBlank(dsKey)) {
83-
// 初始化该key对应的数据源
84-
initDataSource(dsKey);
72+
if (StringUtil.isBlank(currentContext) && StringUtil.isNotBlank(dsKey)) {
73+
// 添加该key对应的数据源
74+
manager.addDataSource(dsKey);
75+
// 添加成功则设置此标识为true,用于在finally中判断是否需要清理
76+
requestAddDataSource = true;
8577
// 切换上下文
8678
DynamicDataSourceContextHolder.setDataSourceLookupKey(dsKey);
8779
}
80+
8881
return invocation.proceed();
8982
} catch (Exception e) {
9083
log.error("An exception occurred during the invocation of @SaaS, data source will switch to default.", e);
9184
return invocation.proceed();
9285
} finally {
93-
DynamicDataSourceContextHolder.clearDataSourceLookupKey();
86+
// 如果Request模式切添加数据源成功,则需要做最后的清理
87+
if (requestAddDataSource) {
88+
DynamicDataSourceContextHolder.clearDataSourceLookupKey();
89+
}
9490
}
9591
}
9692

@@ -110,20 +106,4 @@ private String getDsKeyField(MethodInvocation invocation) throws Throwable {
110106
return saas.value();
111107
}
112108

113-
/**
114-
* 根据key初始化数据源
115-
*
116-
* @param dsKey
117-
*/
118-
private void initDataSource(String dsKey) {
119-
Map<String, DataSource> dsMap = dynamicRoutingDataSource.getCurrentDataSources();
120-
// 如果已被缓存则直接返回
121-
if (dsMap != null && dsMap.containsKey(dsKey)) {
122-
return;
123-
}
124-
125-
// 由开发者自行实现此接口来提供数据源
126-
DataSource ds = dynamicDataSourceProvider.createDataSource(dsKey);
127-
dynamicRoutingDataSource.addDataSource(dsKey, ds);
128-
}
129109
}

0 commit comments

Comments
 (0)