Aop&ThreadLocal实现动态数据源切换

AOP(面向切面编程)

概述

面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,它允许开发者在不改变业务逻辑的情况下,通过切面(Aspect)来模块化横切关注点。横切关注点是那些存在于应用程序中多个模块中、并且通常分散在各个模块中的功能,例如日志、事务管理、安全性等。

AOP 主要通过以下几个概念来实现:

  1. 切面(Aspect): 切面是一个模块化的单元,它封装了与横切关注点相关的行为。通常,一个切面由一个或多个通知(Advice)和一个切入点(Pointcut)组成。
  2. 通知(Advice): 通知定义了在什么时候、在何处以及如何应用切面的行为。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(After Returning)、异常通知(After Throwing)和环绕通知(Around)。
  3. 切入点(Pointcut): 切入点是一个表达式,它定义了在哪里应用切面的行为。切入点表达式可以匹配一个或多个连接点(Join Point)。
  4. 连接点(Join Point): 连接点是应用程序执行过程中的一个特定点,例如方法的调用或异常的抛出。切入点定义了在哪里匹配连接点。
  5. 目标对象(Target Object): 目标对象是被一个或多个切面所通知的对象。它是应用程序中的真正业务逻辑。
  6. 织入(Weaving): 织入是将切面与目标对象关联起来的过程。织入可以发生在编译时、类加载时、运行时,或者在方法调用时。

AOP 的优势在于它可以将横切关注点从业务逻辑中分离出来,提高了代码的模块化、可维护性和可重用性。典型的应用场景包括日志记录、事务管理、权限控制等。在 Java 中,Spring 框架提供了强大的 AOP 支持,使得开发者能够方便地实现面向切面编程。

集成到Spring-boot

导入依赖:
<!-->简单满足基础使用 </-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-->功能更加强大</-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
  • spring-boot-starter-aop 是 Spring Boot 提供的一个 Starter(启动器),它简化了在 Spring Boot 项目中使用 AOP 的配置过程。
  • aspectjweaver是AspectJ框架的一部分,它是用于支持 Java 编程语言的 AOP 框架。AspectJ 提供了更丰富的切面表达式语言和更强大的切面功能,包括编译时植入和运行时植入等。

本次的功能使用只需要最简单高效的spring-boot-starter-aop就能实现

注解及使用
  1. **@Aspect:**切面

    • 作用: 用于定义一个切面,将类标识为切面,可以在类里面定义各种增强操作。

    • 示例:

      @Aspect
      public class MyAspect {
          // 切面类的定义
      }
      
  2. **@Pointcut:**切入点

    • 作用: 用于定义可重用的切入点表达式,将方法标识为切入点,把要切入的位置与方法进行绑定,方便后面的通知(before,after等)绑定

    • 示例:

      @Pointcut("execution(* com.example.service.*.*(..))")
      public void myPointcut() {
          // 切入点表达式
      }
      
  3. **@Before:**通知

    • 作用: 在目标方法执行之前执行增强逻辑。

    • 示例:

      @Before("myPointcut()")
      public void beforeServiceMethod() {
          // 增强逻辑
      }
      
  4. **@After:**通知

    • 作用: 在目标方法执行之后执行增强逻辑(不论方法是否正常完成)。

    • 示例:

      @After("myPointcut()")
      public void afterServiceMethod() {
          // 增强逻辑
      }
      
  5. **@AfterReturning:**通知

    • 作用: 在目标方法成功执行后执行增强逻辑。

    • 示例:

      @AfterReturning(pointcut = "myPointcut()", returning = "result")
      public void afterReturningServiceMethod(Object result) {
          // 增强逻辑
      }
      
  6. **@AfterThrowing:**通知

    • 作用: 在目标方法抛出异常后执行增强逻辑。

    • 示例:

      @AfterThrowing(pointcut = "myPointcut()", throwing = "exception")
      public void afterThrowingServiceMethod(Exception exception) {
          // 增强逻辑
      }
      
  7. **@Around:**通知

    • 作用: 在目标方法执行前后进行增强,可以控制目标方法的执行流程。

    • 示例:

      @Around("myPointcut()")
      public Object aroundServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
          // 前置逻辑
          Object result = joinPoint.proceed(); // 调用目标方法
          // 后置逻辑
          return result;
      }
      

这些注解可以组合使用,根据实际需要来实现不同的切面逻辑。例如,可以通过 @Around 注解来实现完全控制目标方法的执行流程。在 AOP 中,这些注解提供了灵活的方式来定义切面,以便实现横切关注点的模块化和可重用。


多数据源

在我的实习的工作中,其中有一次涉及到的一个需求就是将其他不同数据库中的内容同步到当前的一个数据库中来,所以涉及到多个数据源来回切换。具体实现就是实现AbstractRoutingDataSource接口,将当前的数据源与当前线程局部副本ThreadLocal进行绑定。通过Aop面向切面编程设置线程副本变量的值,来切换数据源。

流程图

在这里插入图片描述

yml配置多数据源连接信息

可以配置多个数据源,我这里只配置了本地的两个数据源

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://localhost:3306/test  # 我自己本地的mysql数据库
        username: root
        password: abc123
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://192.168.1.107:3306/test  # 自己本地的Ubuntu上的数据库
        username: root
        password: abc123
        driver-class-name: com.mysql.cj.jdbc.Driver

配置常量类

编写常量类,防止拼写错误或者失误修改

public class DynamicDataSources {
    /**
     * 主库名称
     */
    public static final String MASTER = "master";
    /**
     * 从库名称
     */
    public static final String SLAVE = "slave";

}

多数据源的注册

将ymal配置的多个数据源进行注入,也可以使用一个数据库来专门的存储多个数据库的连接信息,然后直接统一的进行查询注册。

@Configuration
@ConditionalOnClass(SqlSessionFactory.class)
public class DataSourceConfig  {
    /**
     * 获取yml中主库数据源信息
     */
    @Value("${spring.datasource.druid.master.password}")
    private String masterPassword;

    @Value("${spring.datasource.druid.master.username}")
    private String masterUserName;

    @Value("${spring.datasource.druid.master.url}")
    private String masterUrl;

    @Value("${spring.datasource.druid.master.driver-class-name}")
    private String masterDriverClassName;

    public DataSource masterDataSource() {
        DruidDataSource masterDruidDataSource = new DruidDataSource();
        masterDruidDataSource.setUrl(masterUrl);
        masterDruidDataSource.setUsername(masterUserName);
        masterDruidDataSource.setPassword(masterPassword);
        masterDruidDataSource.setDriverClassName(masterDriverClassName);
        return masterDruidDataSource;
    }

    /**
     * 获取yml中从库数据源信息
     */
    @Value("${spring.datasource.druid.slave.password}")
    private String slavePassword;

    @Value("${spring.datasource.druid.slave.username}")
    private String slaveUserName;

    @Value("${spring.datasource.druid.slave.url}")
    private String slaveUrl;

    @Value("${spring.datasource.druid.slave.driver-class-name}")
    private String slaveDriverClassName;

    public DataSource slaveDataSource() {
        DruidDataSource slaveDruidDataSource = new DruidDataSource();
        slaveDruidDataSource.setUrl(slaveUrl);
        slaveDruidDataSource.setUsername(slaveUserName);
        slaveDruidDataSource.setPassword(slavePassword);
        slaveDruidDataSource.setDriverClassName(slaveDriverClassName);
        return slaveDruidDataSource;
    }

    /**
     * 配置数据源交给spring管理
     */
    @Bean
    public DataSource dataSource() {
        DynamicDataSource dataSource = new DynamicDataSource();
        final DataSource masterDataSource = masterDataSource();
        final DataSource slaveDataSource = slaveDataSource();
        Map<Object,Object> dataSourceMap  = new HashMap<>();
        dataSourceMap.put(DynamicDataSources.MASTER, masterDataSource);
        dataSourceMap.put(DynamicDataSources.SLAVE, slaveDataSource);
        dataSource.setTargetDataSources(dataSourceMap);
        //设置默认数据源
        dataSource.setDefaultTargetDataSource(masterDataSource);
        return dataSource;
    }
    /**
     * 创建JdbcTemplate交给spring管理
     */
    @Bean
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(dataSource());
    }

}

设置ThreaLocal工具类

利用ThreadLocal的技术,保证线程切换数据源的安全性,不会去影响其他线程甚至整个程序的数据源调整。通过设置ThreadLocal来将当前线程与目标的数据库进行绑定

public class DynamicDataSourceContextHolder {
    /**
     * 每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    /**
     * 设置当前线程需要使用的数据源id(和当前线程绑定)
     */
    public static void setDataSourceId(final String  dataSourceId){
        CONTEXT_HOLDER.set(dataSourceId);
    }
    /**
     * 获取当前线程使用的数据源id
     */
    public static String getDataSourceId(){
        return CONTEXT_HOLDER.get();
    }
    /**
     * 清空当前线程使用的数据源id
     */
    public static  void clearDataSourceId(){
        CONTEXT_HOLDER.remove();
    }
}

实现多数据源接口

AbstractRoutingDataSource 是 Spring Framework 提供的一个抽象类,它实现了 Spring 的 javax.sql.DataSource 接口,并且支持动态地切换目标数据源。AbstractRoutingDataSource 的子类需要实现一个方法,即 determineCurrentLookupKey(),该方法返回当前线程应该使用的数据源的标识。Spring 将根据这个标识来选择具体的数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceId();
    }
}

自定义注解

自定义注解是方便我们切换数据源,只需要在需要切换数据源的方法上添加自定义注解并且设置dataSourceId的值,便可以动态的切换数据源。

@Retention(RetentionPolicy.RUNTIME) // 指定注解的保留策略
@Target({ElementType.TYPE,ElementType.METHOD})  // 用于指定自定义注解可以应用的程序元素类型
public @interface DataSourceDynamicSwitch {
    String  dataSourceId() default DynamicDataSources.MASTER; // 设置了一个默认的数据值为主数据库
}

@Retention 是 Java 中的一个元注解,用于指定注解的保留策略,即该注解应该在什么级别保存。@Retention 可以有三个取值:

  1. SOURCE:
    • 注解仅在源代码级别保留,编译器将丢弃这种注解,不会包含在编译后的类文件中。
    • 这意味着在运行时无法通过反射获取这种注解。
  2. CLASS:
    • 注解在编译时保留,会包含在编译后的类文件中,但在运行时无法通过反射获取。
    • 默认值为 CLASS
  3. RUNTIME:
    • 注解在运行时保留,可以通过反射获取。
    • 这是最常用的保留策略,用于在运行时处理注解。

@Target 是 Java 中的一个元注解,用于指定自定义注解可以应用的程序元素类型,即注解可以放在哪些地方使用。@Target 可以有多个取值,每个取值表示一种程序元素类型。以下是常见的 @Target 取值:

  1. ElementType.TYPE:
    • 表示注解可以应用在类、接口(包括注解类型)或枚举声明。
  2. ElementType.FIELD:
    • 表示注解可以应用在字段(包括枚举常量)声明。
  3. ElementType.METHOD:
    • 表示注解可以应用在方法声明。
  4. ElementType.PARAMETER:
    • 表示注解可以应用在参数声明。
  5. ElementType.CONSTRUCTOR:
    • 表示注解可以应用在构造方法声明。
  6. ElementType.LOCAL_VARIABLE:
    • 表示注解可以应用在局部变量声明。
  7. ElementType.ANNOTATION_TYPE:
    • 表示注解可以应用在注解类型声明(即元注解)。
  8. ElementType.PACKAGE:
    • 表示注解可以应用在包声明。

AOP切换数据源

使用AOP操作,当我们在调用被自定义注解修饰的方法时候,进行数据源的切换。

@Aspect
@Component
public class DynamicDataSourceSwitchAspect {

    @Pointcut("@annotation(com.test.aop.ano.DataSourceDynamicSwitch)")
    public void pointcut() {
    }
    
    /**
     * 通过切面切换数据源 通过threadlocal 一个方法会绑定一个线程 数据源存储在此线程里面
     **/
    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println("开始切换数据源 .....");
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //先获取方法上注解
        DataSourceDynamicSwitch annotation = method.getAnnotation(DataSourceDynamicSwitch.class);
        if (annotation == null) {
            //方法上没有,再获取类上的注解
            annotation = method.getClass().getClass().getAnnotation(DataSourceDynamicSwitch.class);
            if (annotation == null) return;
        }
        //获取注解上指定切换的数据源
        String dataSourceId = annotation.dataSourceId();
        //通过DynamicDataSourceContextHolder上下文对象设置数据源ID
        DynamicDataSourceContextHolder.setDataSourceId(dataSourceId);
    }

    /**
     *  清除掉当前线程数据源,下次线程进来防止干扰默认数据源
     **/
    @After("pointcut()")
    public void doAfter(){
        DynamicDataSourceContextHolder.clearDataSourceId();
    }

}

方法中的使用

在调用不同数据库的方法头上,使用自定义的注解,并且将dataSourceId的值设置为常量类的设置对应的常量成员。

@RestController
@RequestMapping("/user")
public class UserController {

   @Resource
   AgentServiceImpl agentService;

   @Resource
   UserServiceImpl userService;

    @GetMapping("/master")
    @DataSourceDynamicSwitch(dataSourceId = "master")  // 切换数据源
    public String masterTest() {
        System.out.println("userService.getById(1) = " + userService.getById(1));
        return "success !";
    }

    @GetMapping("/slave")
    @DataSourceDynamicSwitch(dataSourceId = "slave")  // 切换数据源
    public String slaveTest() {
        System.out.println("agentService.getById(1) = " + agentService.getById(1));
        return "success !";
    }
}