前言
上一部分我们实现了一个简单的动态多数据源配置。详情请看上一篇文章:
【Springboot】 实现DynamicDataSource配置多数据源 - Part1
我们说过,这个时候我们其实只能在调用mapper之前手动的去修改数据源名称来获取别的数据源。我们这次考虑使用注解+切面技术来让他动态的去设置数据源名称。
具体实现
1. 注解类DataSource
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "";
}
定义了一个叫做DataSource的注解,该注解只有一个value属性,属性定义为数据源名称
2. AspectJ切面实现
@Component
@Aspect
public class DataSourceAspect {
//定义切面的切入点,当前是所有以Mapper结尾的类的方法
@Pointcut("execution(* com..*Mapper.*(..))")
public void aspect() {
}
//在切面方法执行前,从当前的线程安全类中获取到数据源名称
@Before("aspect()")
public void before(JoinPoint joinPoint) throws InvocationTargetException, IllegalAccessException {
//通过一些逻辑从当前方法上获取当前要切库所用到的数据源
//这里就是通过当前切入的方法获得它的注解,并获取到注解的value作为key
DynamicDataSourceContext.setDataSourceName(key);
}
//在数据库方法执行完毕后,清空线程安全类中ThreadLocal的key
@After("aspect()")
public void after() {
DynamicDataSourceContext.clearDataSourceName();
}
这种方法比较简单,也只需要一个类就能够实现AspectJ的切面。但是需要引入AspectJ的包。并且在获取当前Mapper方法或者该方法所在类的@DataSource注解的时候需要比较多的步骤。切入点也可以自己定义,可能是你需要的一些特别的切入点。
3. Springboot AOP实现
这个方法不需要多余的包,只需要Springboot starter的aop依赖即可。他需要如下两个类
- Interceptor类
public class AnnotationInterceptor implements MethodInterceptor {
private Map<Method, DataSource> methodCacheMap = new ConcurrentHashMap<>();
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
try {
String dataSource = this.determineDataSource(methodInvocation);
if(dataSource == null || !DynamicDataSourceContext.containsDataSource(dataSource)){
String defaultDataSource = DynamicDataSourceContext.defaultDataSourceKey;
dataSource = defaultDataSource;
}
DynamicDataSourceContext.setDataSourceName(dataSource);
return methodInvocation.proceed();
}finally {
DynamicDataSourceContext.clearDataSourceName();
}
}
private String determineDataSource(MethodInvocation invocation){
Method method = invocation.getMethod();
if(this.methodCacheMap.containsKey(method)){
return this.methodCacheMap.get(method).value();
}else{
DataSource dataSource = method.isAnnotationPresent(DataSource.class) ?
//get annotation.
method.getAnnotation(DataSource.class) :
//if can't get,try to get from current class or interface.
AnnotationUtils.findAnnotation(method.getDeclaringClass(), DataSource.class);
this.methodCacheMap.put(method, dataSource);
return (dataSource != null) ? dataSource.value() : null;
}
}
}
定义了一个Interceptor,当拦截到了指定的方法的时候就会先执行该类。执行的第一步就是确认数据源名称。通过determineDataSource()方法来取得。先看当前的methodCacheMap中是否注册过该方法,如果注册过,就通过方法获取到DataSource的注解对象。并通过注解对象获取到value(数据源名称)。如果找不到,就去找当前方法是否又@DataSource的注解,如果有,就是用方法注解的value,如果没有就找方法所在的类是否有@DataSource注解,如果有就使用。最后把方法和注解对象放入methodCacheMap中(注册)。下次见到这个方法就能直接找到该方法用到的数据源名称了。
如果通过注解获取不到数据源名称,或者获取到的数据源名称在DynamicDataSourceContext中没有找到。那么就用DynamicDataSourceContext定义的默认数据源名称返回。返回的数据源名称DynamicDataSourceContext.setDataSourceName设置进当前线程的ThreadLocal。然后点用methodInvocation.proceed()返回结果,最后在finally中重置ThreadLocal。
- Advisor类
@EqualsAndHashCode(callSuper = false)
public class AnnotationAdvisor extends AbstractPointcutAdvisor {
private transient Advice advice;
private transient Pointcut pointcut;
public AnnotationAdvisor(AnnotationInterceptor annotationInterceptor) {
this.advice = annotationInterceptor;
this.pointcut = this.buildPointcut();
}
@Override
public Advice getAdvice() {
return this.advice;
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
private Pointcut buildPointcut(){
Pointcut classPointcut = new AnnotationMatchingPointcut(DataSource.class, true);
Pointcut methodPointcut = AnnotationMatchingPointcut.forMethodAnnotation(DataSource.class);
return new ComposablePointcut(classPointcut).union(methodPointcut);
}
}
定义了一个Advisor类,在该类中定义了一个切入点,然后将切面(annotationInterceptor)和切入点(pointcut)绑定起来。这个切入点就是找所有被@DataSource注解标识的类和方法。
- 修改Mapper方法
@Mapper
public interface TestMapper {
@Select("XXXXXX")
void Test();
@Insert("XXXXXX")
@Datasource("db-test1")
void Test1();
@Select("XXXXXX")
@Datasource("db-test2")
void test2();
@Select("XXXXXX")
@Datasource("db-test3")
void test3();
}
现在就像我们上篇文章开篇说的一样,我们能够将@DataSource注解标识在类上面,也可以标识在方法上面。value就是数据源名称。
这样,无需我们手动设置,也可以实现数据源动态切换了。
弊端
-
不能同时访问多个数据源:在使用 DynamicDataSource 进行动态数据源切换时,同一时间只能访问一个数据源,不能同时访问多个数据源。
-
不支持事务嵌套:在使用 DynamicDataSource 进行动态数据源切换时,如果在一个事务中需要访问多个数据源,那么就需要进行事务管理,而 DynamicDataSource 并不支持事务嵌套。
-
切面中定义了methodCacheMap来存储了每个方法对应的数据源名称。这样,即使多线程访问同一个方法, 同方法也不可能切换数据源。意味着每个方法针对的数据源写死了。例如,A,B库都存在同样结构的USER表。一个A方法是查询USER表的内容,第一次进来的是查询数据源A库的,所以methodCacheMap存储了A方法对应A库数据源名称的键值对,所以即使另一个线程进来想要调用A方法但是用B数据源的话,就没法从新设定。所以如果有这个需求,就需要修改methodCacheMap的逻辑甚至不用它。
优点
- 简单易用:使用 DynamicDataSource 和 ThreadLocal 进行动态数据源切换,配置相对简单,易于上手。
- 可扩展性强:通过继承 AbstractRoutingDataSource 类,可以实现自定义的数据源路由策略。并且,由于采用了抽象类,扩展性也比较好。
- 线程安全:使用 ThreadLocal 来存储当前数据源的名称,可以避免多线程之间数据源切换的混乱和不成功的情况。
更多需求
麻烦事又来了,我们之前是使用了yml配置文件读取的方式来初始化需要的数据源。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://****:3306/test
username: root
password: root
mutil-datasource:
connection:
- dbName: db-test1
dbDriver: com.microsoft.sqlserver.jdbc.SQLServerDriver
dbUrl: jdbc:sqlserver://****:1433;DatabaseName=db_test1
dbUsername: root
dbPassword: root
- dbName: db-test2
dbDriver: com.sybase.jdbc4.jdbc.SybDriver
dbUrl: jdbc:sybase:Tds:****:5000/db_test2
dbUsername: root
dbPassword: root
可以看到,我们数据库的用户名,密码都在里面配置的,这样很大程度上有风险。
现在有个需求,我们需要在参数中读取到两个外部连接,一个是配置数据库连接URL和基本信息的,这个文件中还配置了一个security的文件地址,这个文件地址指向另一个所需的密码配置文件,而密码文件中的密码是加密过的,所以获取到了这个密码还需要使用方法去解密出来才能使用。因此,直接配置明文的方式显然是不可靠的了。
我们之后有时间再跟大家介绍如何来搞定这个比较复杂的需求。