Spring Boot中动态数据源切换

Spring Boot中动态数据源切换

运行版本

  • spring boot 2.2.5.RELEASE
  • 数据源使用spring boot自带的Hikari
  • 数据库连接 mybatis plus (spring data jpa也可行)
  • 不支持事务

步骤说明:

1.新建spring boot项目引入依赖

        <!--添加Web依赖(servlet) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Mybatis Plus 依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        
        <!-- mysql 链接依赖包  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        
        <!-- AOP begin 如果是spring data jpa则不用引入这两个包。jpa中已带有 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.7.4</version>
        </dependency>
        
        <!-- 解决读取配置文件时报错:Spring Boot configuration annotation processor not found in classpath -->
        <!-- 导入配置文件处理器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

2.修改application.yaml配置文件

server:
  port: 9099
spring:
  application:
    name: datasource-switch
  datasource:
    one:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://10.172.246.234:3306/zhouyf?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
    two:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://10.172.246.234:3306/zhouyf2?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
    three:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://10.172.246.234:3306/zhouyf3?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
#读取mybatis的mapper文件
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml

3.创建数据源类,读取配置文件中配置的数据源

@Component
@Data
@ConfigurationProperties(prefix ="spring.datasource")
@EnableConfigurationProperties
public class DBproperties {

    private HikariDataSource one;

    private HikariDataSource two;

    private HikariDataSource three;
    
}

HikariDataSource 中的HikariConfig为数据源可配置的信息,如有需要可以参考配置在配置文件中

4.创建DynamicDataSource类扩展Spring的AbstractRoutingDataSource抽象类,重写 determineCurrentLookupKey() 方法

/**
 * DynamicDataSource扩展Spring的AbstractRoutingDataSource抽象类,重写 determineCurrentLookupKey() 方法
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
     * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好
     *  @param defaultTargetDataSource 默认数据源
     * @param targetDataSources       目标数据源
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }


    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
    
}

4.注册刚创建的实体DynamicDataSource并把数据源信息传进去

@Configuration
public class DynamicDataSourceConfig {

    @Autowired
    private DBproperties properties;

    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        Map targetDataSources =new HashMap<>();
        targetDataSources.put("one", properties.getOne());
        targetDataSources.put("two", properties.getTwo());
        targetDataSources.put("three", properties.getThree());
        //设置one为默认数据源
        return new DynamicDataSource(properties.getOne(),targetDataSources);
    }

}

5.创建注解@UseDataSource并创建该注解的切面

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseDataSource {
    String value() default "";
}


@Slf4j
@Aspect
@Component
public class DataSourceAspect implements Ordered {
    @Pointcut("@annotation(zhouyf.datasource.UseDataSource)")
    public void dataSourcePointCut() {}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        UseDataSource ds = method.getAnnotation(UseDataSource.class);
        if (ds == null) {
            //value为空则使用默认数据源
            DynamicDataSource.setDataSource("one");
            log.warn("----数据源使用----- 当前数据源为:one");
        } else {
            DynamicDataSource.setDataSource(ds.value());
            log.warn("----数据源使用----- 当前数据源为:"+ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
        }
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

6.测试

数据库信息以及其中的数据

image

image

image

spring boot data jpa 使用注意事项

  • 如果 jpa.hibernate.ddl-auto为update或great则会在默认的数据源上创建或修改表结构
  • 可以返回同一实体类(如果数据源2和数据源1同时都有一张表的情况下,会去切换的数据源下找)