所谓动态数据源是指运行时动态的改变数据源,spring boot 默认可以配置一个数据源,要同时支持 mysql 和 oracle 都不太容易,而如果能动态切换数据源,自然也能同时存在数个数据源,且可以随意切换,本文是之前几篇博客的升华,即使用 aop 技术实现动态的修改运行时数据源。
如果你懒得看具体怎么实现,直接拿我的成果吧,在 pom.xml 里添加依赖:
<dependency>
<groupId>top.kpromise</groupId>
<artifactId>dynamic-data-source-spring-boot-starter</artifactId>
<version>0.0.1</version>
</dependency>
如果遇到如下错误:
The bean 'dataSource' could not be registered. A bean with that name has already been defined in class path resource
直接在 application.properties 里新增:spring.main.allow-bean-definition-overriding=true 即可,另外,你还需要配置你额外的数据库,比如:
dynamic.datasource.name=data2
dynamic.datasource.data2.url=jdbc:mysql://localhost:3506/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
dynamic.datasource.data2.username=test
dynamic.datasource.data2.password=test
dynamic.datasource.data2.driver-class-name=com.mysql.cj.jdbc.Driver
这里,name 后面是 list,你可以写 data2,也可以写 data2, data3 等,多个的时候用逗号分割,下面的 dynamic.datasource.data2.xxx 是该数据库的配置信息,此处的 data2 也可以是你自定义的任意值。
然后在 mapper 文件里添加注解,比如:
@Repository
@DataSource(name = "data2")
public interface TestMapper {
int deleteByPrimaryKey(Long id);
int insert(Test record);
int insertSelective(Test record);
@DataSource(name = "test")
Test selectByPrimaryKey(Long id);
int updateByPrimaryKeySelective(Test record);
int updateByPrimaryKey(Test record);
}
其中,类上的注解是针对这个类里所有方法的,而方法上的注解可以覆盖类上的注解,如果配置如上,除了 selectByPrimaryKey 将使用 test 这个数据库外,其余都使用 data2,如果你没有配置 test 这个数据库将使用你默认的数据库。
毫无疑问,这个解决方案完美的兼容已有的代码,且通过 aop 技术实现,没有耦合,如果你删除 pom.xml 里的依赖,顶多只是 application.properties 里显示警告而已,对已有系统侵入最小。下面是具体的实现。
1、首先,application.properties 里的配置是动态的,即 dynamic.datasource.data2.url 等这里的 data2 你可以随意写,但是后面却只能跟 password 等,这个的具体实现请参考:
深入理解 spring boot 自定义属性 的最后部分。
2、关于 aop 请查阅:
spring aop - 面向切面编程 相关的文章。
3、关于如何 使用 idea 打包并上传 jar 包到 maven 中央仓库请参考:
IDEA 打包并上传 jar 包 到 maven 中央仓库
之所以提以上3点,是因为本文是在其基础上实现的,或者说本文说的内容是在其基础上才能写的。其实,spring boot 提供了动态切换数据源的方法,即 AbstractRoutingDataSource 这个类。
AbstractRoutingDataSource
这个类集成自 AbstractDataSource,也是个抽象类,你必须得实现 determineCurrentLookupKey 这个方法,而这个方法的作用就是:返回你所要使用的数据源的 key 值。比如:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
然后你需要将数据库信息注册进去,之后在执行 sql 语句前,会调用 这里的 determineCurrentLookupKey,最终调用:DynamicDataSourceContextHolder.getDataSourceType(); 而 DynamicDataSourceContextHolder 源码如下:
import java.util.ArrayList;
import java.util.List;
class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
static List<String> dataSourceIds = new ArrayList<>();
static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
static String getDataSourceType() {
return contextHolder.get();
}
static void clearDataSourceType() {
contextHolder.remove();
}
static boolean isContainsDataSource(String dataSourceId) {
return dataSourceIds.contains(dataSourceId);
}
}
现在,大致应该明白,当你手动把数据库配置信息注册到 spring 后,在执行 sql 前,会调用 这里的 DynamicDataSourceContextHolder.getDataSourceType(); 以决定连接那个数据库,而 这个方法的值其实是 setDataSourceType 这个方法设置的,setDataSourceType 这个方法 和 clearDataSourceType 这个方法则是开放给 aop 层调用的,aop 层的代码如下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
@Order(-10)
public class DynamicDataSourceAspect {
@Pointcut("@within(DataSource)")
public void dataSource() {
}
private DataSource findDataSource(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource == null) {
try {
dataSource = AnnotationUtils.findAnnotation(signature.getMethod().getDeclaringClass(), DataSource.class);
} catch (Exception e) {
e.printStackTrace();
}
}
return dataSource;
}
@Before("dataSource()")
public void changeDataSource(JoinPoint joinPoint) {
DataSource dataSource = findDataSource(joinPoint);
if (dataSource == null) return;
String databaseName = dataSource.name();
if (!DynamicDataSourceContextHolder.isContainsDataSource(databaseName)) {
log.error("{} === dataSource {} not exists,use default now", joinPoint.getSignature(), databaseName);
} else {
log.debug("use dataSource:" + databaseName);
log.info("{} === use dataSource {} ", joinPoint.getSignature(), databaseName);
DynamicDataSourceContextHolder.setDataSourceType(databaseName);
}
}
@After("dataSource()")
public void clearDataSource(JoinPoint joinPoint) {
DataSource dataSource = findDataSource(joinPoint);
if (dataSource == null) return;
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
以上代码,先判断方法本身是否含有 @DataSource 注解,如果没有则查看类是否有,如果也没有则啥也不执行,如果有,则会在方法执行前调用 setDataSourceType, 传入的参数就是 @DataSource(name="xxx") 里的 xxx,在方法执行结束,则调用 clearDataSourceType 方法,重置使用的数据库信息,以免影响别的 sql 执行。
完整的代码请查看:
https://github.com/ijustyce/dynamic-data-source-spring-boot-starter 关于 AbstractRoutingDataSource 这个类的解读,我后面再补充吧。
评论