springboot动态配置数据源

简介:

项目开发中经常会遇到多数据源同时使用的场景,比如冷热数据的查询等情况,我们可以使用类似现成的工具包来解决问题,但在多数据源的使用中通常伴随着定制化的业务,所以一般的公司还是会自行实现多数据源切换的功能,接下来一起使用实现自定义注解的形式来实现一下

环境配置:
pom依赖导入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-dynamic-datasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-dynamic-datasource</name>
    <description>spring-boot-dynamic-datasource</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter-test</artifactId>
            <version>3.0.3</version>
            <scope>test</scope>
        </dependency>
        <!--连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
    </build>

</project>
yml文件配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave:
        url: jdbc:mysql://localhost:3306/springsecurity?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 
        driver-class-name: com.mysql.cj.jdbc.Driver

在这里可以看到设置了两个数据库,一个是security,一个是springsecurity

在这两个数据库里面创建两个相同的表结构Student,security数据库sql脚本如下;

create table student
(
    name    varchar(15) null,
    email   varchar(35) null,
    address varchar(15) null,
    age     int         null,
    id      int         null
);

INSERT INTO security.student (name, email, address, age, id) VALUES ('master', '3548297839@qq.com', '中国深圳', 18, null);

spring security数据库sql脚本如下;

create table student
(
    name    varchar(15) null,
    email   varchar(35) null,
    address varchar(15) null,
    age     int         null,
    id      int         null
);

INSERT INTO security.student (name, email, address, age, id) VALUES ('slave', '3548297839@qq.com', '中国深圳', 18, null);

mybatis-plus配置不做赘述,提供一个查询所以student的方法;

管理数据源:

我们应用ThreadLocal来管理数据源信息,通过其中内容的get,set,remove方法来获取、设置、删除当前线程对应的数据源,创建一个DataSourceContextHolder类

package com.example.springbootdynamic.config;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();

    /**
     * 获取当前线程的数据源
     *
     * @return 数据源名称
     */
    public static String getDataSource() {
        return DATASOURCE_HOLDER.get();
    }

    /**
     * 设置数据源
     *
     * @param dataSourceName 数据源名称
     */
    public static void setDataSource(String dataSourceName) {
        DATASOURCE_HOLDER.set(dataSourceName);
    }

    /**
     * 删除当前数据源
     */
    public static void removeDataSource() {
        DATASOURCE_HOLDER.remove();
    }
}
重置数据源:

创建 DynamicDataSource 类并继AbstractRoutingDataSource,这样我们就可以重置当前的数据库路由,实现切换成想要执行的目标数据库

package com.example.springbootdynamic.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
        /*
        通过调用父类的方法 setDefaultTargetDataSource和
        setTargetDataSources 来设置默认数据源和目标数据源映射关系
         */
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    /**
     * 这一步是关键,获取注册的数据源信息
     * @return
     * 实现了动态数据源的功能,根据某个上下文中的数据源标识动态地选择目标数据源
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}
注册多个数据源:
package com.example.springbootdynamic.config;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DateSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource dynamicDatasourceMaster() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource dynamicDatasourceSlave() {
        return DruidDataSourceBuilder.create().build();
    }

    /*
    通常用于标识一个Bean定义为首选的候选项。当存在多个相同类型的Bean时,
    Spring容器会选择具有@Primary注解的Bean作为首选项
     */
    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource createDynamicDataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        // 设置默认的数据源为Master
        DataSource defaultDataSource = dynamicDatasourceMaster();
        dataSourceMap.put("master", defaultDataSource);
        dataSourceMap.put("slave", dynamicDatasourceSlave());
        return new DynamicDataSource(defaultDataSource, dataSourceMap);
    }

}
启动类配置:

在启动类的@SpringBootApplication注解中排除DataSourceAutoConfiguration,否则会报错

package com.example.springbootdynamic;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan(basePackages = {"com.example.springbootdynamic.dao"})
public class SpringBootSpringBootDynamicApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootAffairsApplication.class, args);
    }
}
启动项目手动切换数据源测试:

这里我准备了一个接口来验证,传入的 datasourceName 参数值就是刚刚注册的数据源的key

package com.example.springbootdynamic.controller;

import com.example.springbootdynamic.config.DataSourceContextHolder;
import com.example.springbootdynamic.entity.Student;
import com.example.springbootdynamic.service.impl.StudentServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class DynamicSwitchController {
    @Resource
    private StudentServiceImpl studentService;

    @GetMapping("/switchDataSource/{datasourceName}")
    public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
        DataSourceContextHolder.setDataSource(datasourceName);
        List<Student> allStudent = studentService.getAllStudent();
        DataSourceContextHolder.removeDataSource();
        return allStudent.toString();
    }
}
测试结果:

当我们路径是master查询的student结果是master

当我们路径是slave查询的student结果是salve

至此通过执行结果,我们看到传递不同的数据源名称,已经实现了查询对应的数据库数据

注解实现切换数据源:

上边已经成功实现了手动切换数据源,但这种方式顶多算是半自动,我们每次都要通过传入参数来实现数据源的切换,我们可以利用SpringAop特性,通过注解来实现,下边我们来用注解实现切换数据源

定义注解:
package com.example.springbootdynamic.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSelect {
    // 默认数据源
    String value() default "master";
}
实现AOP

定义了@DataSelect注解后,紧接着实现注解的AOP逻辑,拿到注解传递值,然后设置当前线程的数据源

package com.example.springbootdynamic.aopconfig;

import com.example.springbootdynamic.annotation.DataSelect;
import com.example.springbootdynamic.config.DataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Objects;

@Component
@Aspect
public class DSAspect {


    @Pointcut("@annotation(com.example.springbootdynamic.annotation.DataSelect)")
    public void dynamicDataSource() {
    }


    @Around("dynamicDataSource()")
    public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSelect ds = method.getAnnotation(DataSelect.class);
        if (Objects.nonNull(ds)) {
            DataSourceContextHolder.setDataSource(ds.value());
        }
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }

}
测试注解

再添加两个接口测试,使用@DataSelect注解标注,使用不同的数据源名称,内部执行相同的查询条件,看看结果如何?

package com.example.springbootdynamic.controller;

import com.example.springbootdynamic.annotation.DataSelect;
import com.example.springbootdynamic.config.DataSourceContextHolder;
import com.example.springbootdynamic.entity.Student;
import com.example.springbootdynamic.service.impl.StudentServiceImpl;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class DynamicSwitchController {
    @Resource
    private StudentServiceImpl studentService;

    @GetMapping("/switchDataSource/{datasourceName}")
    public String switchDataSource(@PathVariable("datasourceName") String datasourceName) {
        DataSourceContextHolder.setDataSource(datasourceName);
        List<Student> allStudent = studentService.getAllStudent();
        DataSourceContextHolder.removeDataSource();
        return allStudent.toString();
    }

    @DataSelect
    @GetMapping("/getStudentInSecurity")
    public String getStudentBySecurity() {
        List<Student> allStudent = studentService.getAllStudent();
        return allStudent.toString();
    }

    @DataSelect(value = "slave")
    @GetMapping("/getStudentInSpringSecurity")
    public String getStudent() {
        List<Student> allStudent = studentService.getAllStudent();
        return allStudent.toString();
    }
}

通过执行结果,看到通过应用@DataSelect注解也成功的进行了数据源的切换

源码地址:https://github.com/Breeze1203/JavaAdvanced/tree/main/springboot-demo/spring-boot-dynamic-datasource