SpringAop+注解实现SpringBoot动态数据源切换&&AbstractRoutingDataSource源码解析

文章介绍了如何通过SpringAOP和注解实现动态数据源切换,包括环境搭建、依赖配置、动态数据源实现、数据源交由Spring管理以及切换数据源的时机(AOP)。主要涉及AbstractRoutingDataSource的使用,ThreadLocal存储数据源,以及自定义注解和切面实现数据源的切换。同时提到了注解失效的情况及解决办法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

通过Spring AOP + 注解来替换当前线程ThreadLocal中的值,并且通过重写AbstractRoutingDataSource类重写determineCurrentLookUpKey()方法,实现动态数据源切换,满足功能实现的代码0侵入性,并且高度解耦,实现可拔插功能效果

1、环境搭建

导入依赖

<!--springboot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.5.6</version>
</dependency>

<!--aop-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!--mysql-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.23</version>
</dependency>

<!--druid-->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.2.8</version>
</dependency>

配置文件

spring:
  datasource:
    db1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/db1?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8
      username: root
      password: root
      druid:
        test-on-borrow: true
        test-while-idle: true
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost/db2?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8
      username: root
      password: root
      druid:
        test-on-borrow: true
        test-while-idle: true

2、动态数据源

动态数据源的实现说白了是对AbstractRoutingDataSource类原理解析,通过导入mysql-connector-java依赖后会引入mysql的DataSource,而DataSource是跟线程绑定的,通过继承AbstractRoutingDataSource类重写determineCurrentLookUpKey()方法获取数据源,所以需要通过创建ThreadLoacl来存储数据源,以空间换时间实现线程隔离,然后再determineCurrentLookupKey()方法中获取ThreadLocal保存的数据源,从而实现数据源的动态切换。

1、AbstractRoutingDataSource解析

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
  // 自定义数据源时注入
  @Nullable
  private Map<Object, Object> targetDataSources;
  // 自定义数据源时注入
  @Nullable
  private Object defaultTargetDataSource;
  private boolean lenientFallback = true;
  private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
  // 启动时依据spring 加载bean的生命周期时执行afterPropertiesSet方法,通过遍历targetDataSources注入
  @Nullable
  private Map<Object, DataSource> resolvedDataSources;
   // 启动时依据spring 加载bean的生命周期时执行afterPropertiesSet方法,通过defaultTargetDataSource注入
  @Nullable
  private DataSource resolvedDefaultDataSource;

  public AbstractRoutingDataSource() {
  }


  // 在配置动态数据源时,我们会将多个类型为DataSource,以name为key,DataSource为source填充进map后赋值给targetDataSources属性
  public void setTargetDataSources(Map<Object, Object> targetDataSources) {
    this.targetDataSources = targetDataSources;
  }
// 在配置动态数据源时,会指定默认数据源
  public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
    this.defaultTargetDataSource = defaultTargetDataSource;
  }
  
  // 启动后加载
  public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
      throw new IllegalArgumentException("Property 'targetDataSources' is required");
    } else {
      this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
      // 遍历我们配置多数据源,填充到resolvedDataSources中
      this.targetDataSources.forEach((key, value) -> {
        Object lookupKey = this.resolveSpecifiedLookupKey(key);
        DataSource dataSource = this.resolveSpecifiedDataSource(value);
        this.resolvedDataSources.put(lookupKey, dataSource);
      });
      // 默认数据源也会进行填充
      if (this.defaultTargetDataSource != null) {
        this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
      }

    }
  }

  // 真正实现切换数据源的地方
  protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    // 前面我们说到,切换数据源时通过重写了determineCurrentLookupKey方法,方法返回值是通过获取保存在ThreadLocal中值,这个值是在afterPropertiesSet()方法中填充resolvedDataSources的map中充当key,以此获取dataSource
    Object lookupKey = this.determineCurrentLookupKey();
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      // 如果没有重写determineCurrentLookupKey则会使用默认数据源
      dataSource = this.resolvedDefaultDataSource;
    }

    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
      return dataSource;
    }
  }

  // 重写该方法,返回值为resolvedDataSources中的key
  @Nullable
  protected abstract Object determineCurrentLookupKey();
}

2、创建ThreadLocal

/**
 * @DATE: 2023/02/02 11:09
 * @Author: 小爽帅到拖网速
 */
public class DataSourceContextHolder {

  /**
   * 默认数据源
   */
  public static final String DEFAULT_DS = "db1";

  private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

  // 设置数据源名
  public static void set(String dbType) {
    contextHolder.set(dbType);
  }

  // 获取数据源名
  public static String get() {
    if(StringUtils.isBlank(contextHolder.get())) {
      return DEFAULT_DS;
    }
    return contextHolder.get();
  }

  // 清除数据源名
  public static void clear() {
    contextHolder.remove();
  }
}

3、继承AbstractRoutingDataSource类

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

3、创建数据源交由spring管理

自定义两个以上类型为DataSource的Bean,并且通过@ConfigurationProperties绑定yml文件中的配置信息;

通过配置DynamicDataSource动态数据源,设定默认数据源并将创建的多个数据源以数据源名为key,dataSource的bean为value填充进map,并赋值给AbstractRoutingDataSource的targetDataSources字段,注意DataSource在DynamicDataSource中是以k-v的方式保存,所以通过ThreadLocal来保存key,在必要时获取key在重写AbstractRoutingDataSource的determineCurrentLookupKey方法中获取后,在AbstractRoutingDataSource的determineTargetDataSource进行数据源切换

/**
 * @DATE: 2023/02/02 11:04
 * @Author: 小爽帅到拖网速
 */
@Configuration
public class DataSourceConfig {

  /**
   * 数据源db1
   * @return
   */
  @Bean(name = "db1")
  @ConfigurationProperties(prefix = "spring.datasource.db1")
  public DataSource dataSource1(){
    return DataSourceBuilder.create().build();
  }

  /**
   * 数据源 checkentry
   * @return
   */
  @Bean(name = "db2")
  @ConfigurationProperties(prefix = "spring.datasource.db2")
  public DataSource dataSource2(){
    return DataSourceBuilder.create().build();
  }

  /**
   * 配置动态数据源,并指定默认数据源
   * @return
   */
  @Primary
  @Bean(name = "dynamicDS1")
  public DataSource dataSource(){
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    // 设置默认数据源
    dynamicDataSource.setDefaultTargetDataSource(dataSource2());

    // 配置多数据源
    HashMap<Object, Object> dsMap = new HashMap<>();
    dsMap.put("db1",dataSource1());
    dsMap.put("db2",dataSource2());

    dynamicDataSource.setTargetDataSources(dsMap);
    return dynamicDataSource;
  }

  /**
   * 配置@Transactional注解事务
   * @return
   */
  @Bean
  public PlatformTransactionManager transactionManager(){
    return new DataSourceTransactionManager(dataSource());
  }

}

4、切换数据源的时机(Aop)

1、自定义注解

/**
 * @DATE: 2023/02/02 12:38
 * @Author: 小爽帅到拖网速
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DS {
  String value() default "db1"; // 默认数据源
}

/**
 *
 * @DATE: 2022/10/19 17:04
 * @Author: 小爽帅到拖网速
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DsMethod {
  String value() default "db1";
}

2、定义切面

通过Aop搭配注解结合ThreadLocal实现动态切换数据源,无代码侵入性,高度解耦

/**
 * @DATE: 2023/02/02 12:39
 * @Author: 小爽帅到拖网速
 */
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {

  /**
   * 针对类
   * 切换数据源
   * @param point
   */
  @Before("@within(com.xiaoshuang.annoation.DS)")
  public void beforeSwitchDS(JoinPoint point) {
    Class<?> aClass = point.getTarget().getClass();
    // 默认数据源
    String defalutDataSource = DataSourceContextHolder.DEFAULT_DS;
    // 判断是否有DS注解
    if (aClass.isAnnotationPresent(DS.class)){
      DS annotation = aClass.getAnnotation(DS.class);
      // 获取注解中数据源名
      defalutDataSource = annotation.value();
    }
    // 切换数据源
    DataSourceContextHolder.set(defalutDataSource);
  }

  @After("@within(com.xiaoshuang.annoation.DS)")
  public void afterSwitchDS(JoinPoint point){
    // 清空数据源
    DataSourceContextHolder.clear();
  }
  
   /**
   * 针对方法
   * 切换数据源
   * @param point
   */
  @Around("@annotation(com.xiaoshuang.annotation.DsMethod)")
  public void methodSwitchDS(ProceedingJoinPoint point) {
    try {
      MethodSignature sign = (MethodSignature) point.getSignature();
      Method method = sign.getMethod();
      // 获取默认数据源
      String dataSource = DataSourceContextHolder.DEFAULT_DS;
      if (method.isAnnotationPresent(DsMethod.class)) {
        DsMethod annotation = method.getAnnotation(DsMethod.class);
        // 取出注解中的数据源名
        dataSource = annotation.value();
      }
      // 切换数据源
      DataSourceContextHolder.set(dataSource);
      // 执行方法
      point.proceed();
      // 清空数据源
      DataSourceContextHolder.clear();
    } catch (Throwable throwable) {
      logger.error("[DynamicDataSourceMethodAspect] is error,message:{}", ExceptionUtils.getMessage(throwable));
    }
  }
}

5、注解失效的情况

如果在使用spring框架时,出现以下情况,会导致aop拦截失效

@Service
public class Test {

public void a(){
   b();
}

@DsMethod("b")
public void b(){
 }
}

这是因为spring使用的动态代理,通过了解spring的启动流程后可以知道aop是在bean的生命周期注入的代理类,调用a方式是spring创建的代理类,而a方法调用b(),是调用b()的目标类,并不是代理类,所以这里aop会失效,解决方法时在调用b方法时通过获取当前aop上下文代理类并进行强制类型转换,通过获取aop的代理类,才会使aop拦截生效

@Service
public class Test {

public void a(){
  ((Test)AopContext.currentProxy()).b();
}

@DsMethod("b")
public void b(){
 }
}

以上便是动态切换数据源的流程解析,如有解析不当,欢迎在评论区指出,感谢大家阅读与支持!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值