写在前面

在实际工作中我们希望实现多数据源的动态切换,而这一点我们在前面一篇文章中就已经进行了实现,但是对于业务人员来说,更多的则是希望可以通过可视化界面的方式来自己决定使用哪个数据源,进而获取对应的数据。

几个思考

前面我们多次提到AbstractRoutingDataSource抽象类定义了抽象的determineCurrentLookupKey方法,子类只需实现此方法,就可以通过一个Key从Map中获取对应的数据源实例,并执行对应的数据库操作。查看一下之前我们的dynamic-multiple-ds项目,本篇将在此基础上进行实现。由于我们将数据源名称存在DynamicMultipleDataSourceContextHolder这一ThreadLocal中,因此只需修改该变量的值就能实现这个目的。

也就是说首先我们要定义一个方法用于传入新的数据源名称,然后我们可以将这个数据源名称放在Session、Redis或者数据库中,最后在修改DynamicMultipleDataSourceContextHolder的时候从里面取出数据并更新ThreadLocal的值即可。

其次,用户通过页面切换的数据源应该是全局的还是局部的呢?这个应该根据实际的业务来确定,此处假设用户通过页面切换的数据源是全局的,因此后续我们在定义切面的时候不再通过注解方式进行拦截,而是直接拦截所有的业务处理类。还有如果某个拦截的类里面部分方法上,也设置了之前定义的@MyDataSource注解,那么此时应当以哪个为准?这个也是需要根据实际业务来确定,此处假设后者优先级高于前者,即如果某个类既被全局拦截,又添加了@MyDataSource注解,那么最终还是以@MyDataSource注解指定的数据源名称为准。

再次,我们需要提供一个界面以供用户自行切换数据源,然后展示切换数据源后的数据。

实战

第一步,新建Book实体类:

1
2
3
4
5
6
7
8
public class Book {
private Integer id;
private String name;
private Integer price;
private String description;

//getter、setter和toString方法
}

第二步,修改BookMapper接口的代码为如下所示:

1
2
3
4
5
6
7
8
@Mapper
public interface BookMapper {
@Select("select count(*) from book")
Integer number();

@Select("select * from book")
List<Book> getAllBooks();
}

第三步,修改BookService类的代码为如下所示:

1
2
3
4
5
6
7
8
9
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;

public List<Book> getAllBooks(){
return bookMapper.getAllBooks();
}
}

第四步,由于我们是将用户传入的数据源名称存在Session中,因此先定义一个Session中的Key:

1
2
3
4
5
6
7
8
public interface MultipleDataSourceProvider {
String DEFAULT_DATASOURCE = "master";

//作为Session的存储key
String DS_SESSION_KEY = "ds_session_key";

Map<String, DataSource> loadDataSource();
}

第五步,我们可以将之前MyDataSourceAspect这一切面类中获取MyDataSource注解的逻辑抽离出来,以便后续可以重复调用:

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
@Aspect
@Component
public class MyDataSourceAspect {

@Pointcut("@annotation(com.melody.dynamicmultipleds.annotation.MyDataSource)"
+"||@within(com.melody.dynamicmultipleds.annotation.MyDataSource)")
public void myDS(){};

@Around("myDS()")
public Object around(ProceedingJoinPoint point)throws Throwable {
MyDataSource myDataSource = getMyDataSource(point);
if(Objects.nonNull(myDataSource)){
//myDataSource存在则获取并将其存入DynamicMultipleDataSourceContextHolder中
DynamicMultipleDataSourceContextHolder.setDataSourceName(myDataSource.dataSourceName());
}
try{
return point.proceed();
} finally {
//清空数据源
DynamicMultipleDataSourceContextHolder.clearDataSourceName();
}
}

public MyDataSource getMyDataSource(ProceedingJoinPoint point){
//获取方法签名
MethodSignature signature = (MethodSignature)point.getSignature();
//查找方法上的注解
MyDataSource myDataSource = AnnotationUtils.findAnnotation(signature.getMethod(), MyDataSource.class);
if(myDataSource==null){
myDataSource = AnnotationUtils.findAnnotation(signature.getDeclaringType(), MyDataSource.class);
}
return myDataSource;
}
}

第六步,定义一个用于拦截所有业务方法的全局切面类GlobalDataSourceAspect,里面的代码如下:

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
@Aspect
@Component
public class GlobalDataSourceAspect {
@Autowired
private HttpSession session;

/**
* 拦截service包下面的所有类的所有方法
*/
@Pointcut("execution(* com.melody.dynamicmultipleds.service.*.*(..))")
public void globalDS(){};

@Around("globalDS()")
public Object around(ProceedingJoinPoint point){
String dataSourceName = (String)session.getAttribute(MultipleDataSourceProvider.DS_SESSION_KEY);
DynamicMultipleDataSourceContextHolder.setDataSourceName(dataSourceName);
try {
return point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}finally {
//清空数据源
DynamicMultipleDataSourceContextHolder.clearDataSourceName();
}
return null;
}
}

第七步,定义一个名为DataSourceController的类,在里面提供两个方法,一个用于修改数据源名称,另一个用于查询所有的books信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
public class DataSourceController {
private static final Logger logger = LoggerFactory.getLogger(DataSourceController.class);
/**
* 修改数据源名称
* @param dsName
* @param session
* @return
*/
@PostMapping("/dsName")
public void updateDataSourceName(String dsName, HttpSession session){
session.setAttribute(MultipleDataSourceProvider.DS_SESSION_KEY,dsName);
logger.info("数据源切换为{}",dsName);
}

@Autowired
private BookService bookService;

@GetMapping("/books")
public List<Book> getAllBooks(){
return bookService.getAllBooks();
}
}

第八步,在resources/static目录下新建一个名为jquery-3.6.0.js的js文件,里面的内容可以从 这里 获取。接着在该目录下定义一个名为index.html的页面:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>多数据源可视化切换</title>
<script src="jquery-3.6.0.js"></script>
</head>
<body>
<div>
请选择使用的数据源:
<select name="" id="" onchange="dsChange(this.options[this.options.selectedIndex].value)">
<option value="请选择">请选择</option>
<option value="master">master</option>
<option value="slave">slave</option>
</select>
</div>
<div id="result">

</div>
<button onclick="loadData()">加载数据</button>
<script>
function loadData() {
$.get("/books",function (data) {
$("#result").html(JSON.stringify(data));
})
};
function dsChange(value) {
$.post("/dsName",{
dsName: value
})
}
</script>
</body>
</html>

前端页面的代码也较为简单,即让用户手动选择数据源,然后点击下方的加载数据按钮,即可通过新的数据源来加载数据。实际上选择数据源的时候就是请求了/dsName这一API;而加载数据则是请求了/books这一API。

第九步,测试。启动项目,访问http://localhost:8080/index.html页面,选择“master”,可以看到控制台也输出对应信息了:

之后点击加载数据按钮,可以看到首页已经将master这一数据源数据查询出来了:

接着我们再选择“slave”,可以看到控制台也输出对应信息了:

第十步,确定全局拦截和注解拦截业务顺序。由于前面说过,如果某个类既被全局拦截,又添加了@MyDataSource注解,那么最终还是以@MyDataSource注解指定的数据源名称为准。也就是说全局拦截先执行,@MyDataSource注解后执行才能达到既定目的,而配置Bean的顺序可以使用@Order注解来实现,其中@Order注解中设置的数字越小表示优先级越高,越先执行。所以反映到@Order注解上面,要想实现既定目标,那么全局拦截切面类上的@Order注解中设置的数字就要小于@MyDataSource注解所定义切面上的@Order注解中设置的数字值。

可以将全局拦截类GlobalDataSourceAspect上的@Order注解值设置为1;@MyDataSource注解所对应的切面类MyDataSourceAspect上的@Order注解值设置为2,这样就可以让@MyDataSource注解的优先级高于全局。然后在BookService类中的getAllBooks方法中添加@MyDataSource注解,指定数据源为master:

1
2
3
4
5
6
7
8
9
10
@Service
public class BookService {
@Autowired
private BookMapper bookMapper;

@MyDataSource("master")
public List<Book> getAllBooks(){
return bookMapper.getAllBooks();
}
}

之后重启项目,可以发现此时无论用户如何切换数据源,最终用户调用的都是master这一数据源。