一、tienchin健身系统下的技术点复现--动态数据源

发布时间 2023-06-04 23:02:36作者: 山沉

一、tienchin健身系统下的技术点复现--动态数据源

自定义一个注解@DynamicDatasource,可以配置在方法或者类上面,标识方法或者类中的所有方法都使用某一个数据源。

使用数据源是使用ThreadLocal获取数据源名称的,当然要先存入到ThreadLocal中

自定义切面,使用环绕通知,拦截@DynamicDatasource注解的方法或者类中的所有方法的时候,将@DynamicDatasource注解所对应的数据源注入到ThreadLocal中。

最后,当Mapper执行方法时,需要DataSource,它会自动去AbstracRoutingDatasource类中去查找目标数据源。我们只需要在重写的方法determineCurrentLookupKey方法中,返回ThreadLocal的值即可。

1、@annotation注解切换数据源

1.1 引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.18</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

1.2 配置application.yml

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      master:
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
      slave:
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/tesdb2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
    # 初始连接数
    initialSize: 5
    # 最小连接池数量
    minIdle: 10
    # 最大连接池数量
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置连接超时时间
    connectTimeout: 30000
    # 配置网络超时时间
    socketTimeout: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    # 配置一个连接在池中最大生存的时间,单位是毫秒
    maxEvictableIdleTimeMillis: 900000
    # 配置检测连接是否有效
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    webStatFilter:
      enabled: true
    statViewServlet:
      enabled: true
      # 设置白名单,不填则允许所有访问
      allow:
      url-pattern: /druid/*
      # 控制台管理用户名和密码
      login-username: tienchin
      login-password: 123456
    filter:
      stat:
        enabled: true
        # 慢SQL记录
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: true
      wall:
        config:
          multi-statement-allow: true

1.3 使用ThreadLocal配置获取动态数据源

这个类就是把ThreadLocal的数据源名称给AbstracRoutingDatasource,它会找到对应的数据源。

/**
 * 功能描述 把ThreadLocal的数据源
 *
 * @author [山沉]
 * @个人博客 [https://choleen95.github.io/]
 * @博客 [https://www.cnblogs.com/Choleen/]
 * @since [2023/6/3 22:49]
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDatasourceContextHolder.getDatasourceType();
    }
}

配置ThreadLocal工具类

/**
 * 功能描述 这个类用来存储当前线程所使用的数据源名称
 *
 * @author [山沉]
 * @个人博客 [https://choleen95.github.io/]
 * @博客 [https://www.cnblogs.com/Choleen/]
 * @since [2023/6/3 22:50]
 */
public class DynamicDataSourceContextHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
   private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
   /**
    * 单线程获取数据源名称
    *
    * @author 山沉
    * @date 2023/6/3 22:54
    * @return {@link String}
    */
   public static String getDatasourceType(){
       return CONTEXT_HOLDER.get();
   }

   /**
    * 注入数据源对应的数据源名称
    *
    * @param type 数据源对应的key
    * @author 山沉
    * @date 2023/6/3 22:55
    */
   public static void setDatasourceType(String type) {
       LOGGER.info("切换到 {} 数据源了", type);
       CONTEXT_HOLDER.set(type);
   }

   /**
    * 清除ThreadLocal中的数据源名称
    *
    * @author 山沉
    * @date 2023/6/3 22:55
    */
   public static void clearDatasourceType() {
       CONTEXT_HOLDER.remove();
   }

}

1.4 创建@DynamicDatasource注解

/**
 * 功能描述 动态数据源注解
 *
 * @author [山沉]
 * @个人博客 [https://choleen95.github.io/]
 * @博客 [https://www.cnblogs.com/Choleen/]
 * @since [2023/6/3 23:21]
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {
	/**
     * 如果一个方法上使用了 @DynamicDataSource ,但是没有指定数据源名称,默认使用 master 的数据源名称
     *
     * @author 山沉
     * @date 2023/6/4 10:25
     * @return {@link String}
     */
    String vlaue() default DataSourceType.DEFAULT_DS_TYPE;
}

1.5 创建DynamicAspect切面

/**
 * 功能描述 @DynamicDatasource注解 对应的切面,拦截此注解的方法,类中的方法。设置数据源
 *
 * @author [山沉]
 * @个人博客 [https://choleen95.github.io/]
 * @博客 [https://www.cnblogs.com/Choleen/]
 * @since [2023/6/3 23:26]
 */
@Aspect
@Component
public class DynamicAspect {

    /**
     * @annotation(com.example.datasource_tienchin.annotaion.DynamicDatasource) 表示方法上有 @DynamicDatasurce 注解就将方法拦截下来
     * @within(com.example.datasource_tienchin.annotaion.DynamicDatasource) 表示如果类上面有 @DynamicDatasource 注解,就将类中的方法拦截下来
     * @author 山沉
     * @date 2023/6/3 23:34
     */
    @Pointcut("@annotation(com.example.datasource_tienchin.annotaion.DynamicDataSource) || @within(com.example.datasource_tienchin.annotaion.DynamicDataSource)")
    public void customerPc() {}

    @Around("customerPc()")
    public void around(ProceedingJoinPoint pjp) {
        DynamicDataSource datasource = getDatasource(pjp);
        if (datasource != null) {
            String dataSourceName = datasource.vlaue();
            DynamicDataSourceContextHolder.setDatasourceType(dataSourceName);
        }
        try {
            return pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            throw throwable;
        }finally {
            // 最后清除掉数据源名称
            DynamicDataSourceContextHolder.clearDatasourceType();
        }
    }

    private DynamicDataSource getDatasource(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 方法上找注解
        DynamicDataSource dynamicDatasource = AnnotationUtils
                .findAnnotation(methodSignature.getMethod(), DynamicDataSource.class);
        if (dynamicDatasource != null) {
            // 说明方法上有注解
            return dynamicDatasource;

        }
        // 为空,在类上找注解
        return  AnnotationUtils
                .findAnnotation(methodSignature.getDeclaringType(), DynamicDataSource.class);

    }
}

1.6 配置ConfigurationProperties

/**
 * 功能描述 解析application.yml中的配置,生成数据源
 *
 * @author [山沉]
 * @个人博客 [https://choleen95.github.io/]
 * @博客 [https://www.cnblogs.com/Choleen/]
 * @since [2023/6/4 9:25]
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DruidProperties {
    /**
     * 数据源类型
     */
    private String type;
    /**
     *驱动名称
     */
    private String driverClassName;
    /**
     * 动态数据源配置 自定义
     */
    private Map<String, Map<String,String>> druid;
    /**
     * 初始连接数
     */
    private Integer initialSize;
    /**
     * 最小连接池数量
     */
    private Integer minIdle;
    /**
     * 最大连接池数量
     */
    private Integer maxActive;
    /**
     * 配置获取连接等待超时的时间
     */
    private Integer maxWait;
    /**
     * 配置连接超时时间
     */
    private Integer connectTimeout;
    /**
     * 配置网络超时时间
     */
    private Integer socketTimeout;

    /**
     *  在外部构造一个DruidDatasource 对象,这个对象只包含三个核心属性 url、password、username
     *  在此方法中,设置公有属性,当然也可以自定义数据源的属性。
     *
     * @param druidDataSource 数据源对象
     * @author 山沉
     * @date 2023/6/4 9:39
     * @return {@link DataSource}
     */
    public DataSource dataSource(DruidDataSource druidDataSource) {
        druidDataSource.setInitialSize(initialSize);
        druidDataSource.setMinIdle(minIdle);
        druidDataSource.setMaxActive(maxActive);
        druidDataSource.setMaxWait(maxWait);
        druidDataSource.setConnectTimeout(connectTimeout);
        druidDataSource.setSocketTimeout(socketTimeout);

        return druidDataSource;
    }

若不用此@ConfigurationProperties,可以使用@Value("${spring.datasource.type}") 此种类型。

在用 @ConfigureationProperties 中,会有提示 Not registered via @EnableConfigurationProperties官网 中给出了解决办法

  • 引入依赖
  • 加入spring容器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>