【Springboot】 实现DynamicDataSource配置多数据源 - Part2

前言

上一部分我们实现了一个简单的动态多数据源配置。详情请看上一篇文章:

【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的文件地址,这个文件地址指向另一个所需的密码配置文件,而密码文件中的密码是加密过的,所以获取到了这个密码还需要使用方法去解密出来才能使用。因此,直接配置明文的方式显然是不可靠的了。

    我们之后有时间再跟大家介绍如何来搞定这个比较复杂的需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值