Spring Boot 动态数据源

目录

前言

前置环境

pom

yml

Entity

Dao

枚举类

数据源

AOP

Controller

启动类

演示


前言

大多数系统中,都需要数据库来持久化数据,在大多数情况下,一个系统只需要配置一个数据源便能够完成所有业务的查询,保存操作。也存在一个系统需要多个数据源的情况,不同的数据源对应不同的业务操作,这种场景下配置多个数据源,并且在代码中维护多套dao层就可以了。

还存在一种业务场景,所有的业务操作都是一样的,只有操作的数据源的不同,如果用多套dao层来实现,由于业务操作都一样,会出现多块一模一样的代码,这样的冗余代码是我们不希望看到,不利于维护。这种业务场景就很适合用动态数据源来实现。

可以使用 AbstractRoutingDataSource + ThreadLocal + AOP 来实现动态数据源切换

前置环境

JDK8 + SringBoot2 + MySQL8

分别创建数据库 test1 test2

分别在两个数据库中创建 user 表

create table user (
    id int auto_increment primary key,
    username varchar(255),
    password varchar(255)
);

在test1.user 表中插入数据

insert into user(username, password) values('张三', '123456');

在test2.user 表中插入数据

insert into user(username, password) values('李四', '123456');

pom

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    <dependencies>

yml

server:
  port: 8888

spring:
  datasource:
    primary:
      jdbc-url: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    secondary:
      jdbc-url: jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    show-sql: true
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

Entity

UserEntity

@Entity
@Table ( name = "user")
public class UserEntity {
    private Integer id;
    private String username;
    private String password;

    @Id
    @Column ( name = "id" )
    public int getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Basic
    @Column ( name = "username" )
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Basic
    @Column ( name = "password" )
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

Dao

 IUserDao

@Repository (value = IUserDao.DAO_BEAN_NAME )
public interface IUserDao extends JpaRepository<UserEntity, Long> {
    String DAO_BEAN_NAME = "userDao";
}

枚举类

枚举动态数据源,提高代码可读性

DataSourceEnums

public enum DataSourceEnums {
    PRIMARY,
    SECONDARY;

    static {
        set = Arrays.stream(values()).
                map(e -> e.name()).collect(Collectors.toSet());
    }

    private static Set<String> set;

    public static boolean isValid(String dataSource) {
        return set.contains(dataSource);
    }
}

数据源

线程切换数据源上下文,每个请求线程都维护一个自己的当前数据源变量

DynamicDataSourceContenxtHolder

public class DynamicDataSourceContextHolder {

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源变量
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType){
        System.out.printf("切换到{%s}数据源", dataSourceType);
        CONTEXT_HOLDER.set(dataSourceType);
    }

    /**
     * 获取数据源变量
     * @return
     */
    public static String getDataSourceType(){
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType(){
        CONTEXT_HOLDER.remove();
    }
}

DynamicDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
        super.afterPropertiesSet();
    }

    /**
     * 根据Key获取数据源的信息
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

DynamicDatasourceConfig

记得将类中的两个包路径修改成自己项目的包路径

REPOSITORY_PACKAGE

ENTITY_PACKAGE

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories (
        basePackages = DynamicDatasourceConfig.REPOSITORY_PACKAGE,
        entityManagerFactoryRef = "dynamicEntityManagerFactory",
        transactionManagerRef = "dynamicTransactionManager"
)
public class DynamicDatasourceConfig {

    //--------------数据源配置-------------------

    @Bean(name="primary")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name="secondary")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier ("primary") DataSource primaryDataSource, @Qualifier ( "secondary" )DataSource secondaryDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnums.PRIMARY.name(), primaryDataSource);
        targetDataSources.put(DataSourceEnums.SECONDARY.name(), secondaryDataSource);
        return new DynamicDataSource(primaryDataSource, targetDataSources);
    }

    /**
     * 该方法仅在需要使用JdbcTemplate对象时选用
     *
     * @param dataSource 注入名为dynamicDataSource的bean
     * @return 数据源JdbcTemplate对象
     */
    @Bean(name = "dynamicJdbcTemplate")
    public JdbcTemplate jdbcTemplate(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //-------------jpa配置---------------

    static final String REPOSITORY_PACKAGE = "com.your.dao";
    private static final String ENTITY_PACKAGE = "com.your.entity";

    /**
     * 扫描spring.jpa.dynamic开头的配置信息
     *
     * @return jpa配置信息
     */
    @Primary
    @Bean (name = "dynamicJpaProperties")
    @ConfigurationProperties (prefix = "spring.jpa")
    public JpaProperties jpaProperties() {
        return new JpaProperties();
    }

    /**
     * 获取主库实体管理工厂对象
     *
     * @param dynamicDataSource 注入名为dynamicDataSource的数据源
     * @param jpaProperties     注入名为dynamicJpaProperties的jpa配置信息
     * @param builder           注入EntityManagerFactoryBuilder
     * @return 实体管理工厂对象
     */
    @Primary
    @Bean(name = "dynamicEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(
            @Qualifier ("dynamicDataSource") DataSource dynamicDataSource,
            @Qualifier("dynamicJpaProperties") JpaProperties jpaProperties,
            EntityManagerFactoryBuilder builder
    ) {
        return builder
                // 设置数据源
                .dataSource(dynamicDataSource)
                // 设置jpa配置
                .properties(jpaProperties.getProperties())
                // 设置实体包名
                .packages(ENTITY_PACKAGE)
                // 设置持久化单元名,用于@PersistenceContext注解获取EntityManager时指定数据源
                .persistenceUnit("dynamicPersistenceUnit").build();
    }

    /**
     * 获取实体管理对象
     *
     * @param factory 注入名为dynamicEntityManagerFactory的bean
     * @return 实体管理对象
     */
    @Primary
    @Bean(name = "dynamicEntityManager")
    public EntityManager entityManager(@Qualifier("dynamicEntityManagerFactory") EntityManagerFactory factory) {
        return factory.createEntityManager();
    }

    /**
     * 获取主库事务管理对象
     *
     * @param factory 注入名为dynamicEntityManagerFactory的bean
     * @return 事务管理对象
     */
    @Primary
    @Bean(name = "dynamicTransactionManager")
    public JpaTransactionManager transactionManager(@Qualifier("dynamicEntityManagerFactory") EntityManagerFactory factory) {
        return new JpaTransactionManager(factory);
    }

}

AOP

对需要动态切换数据源的请求做切面,编写切换数据源逻辑

记得将切点表达式换成自己项目的路径

 @Pointcut ("execution(* com.your.controller.DynamicController.*(..))")

 DataSourceAspect

@Aspect
@Component
public class DataSourceAspect {

    @Pointcut ("execution(* com.your.controller.DynamicController.*(..))")
    public void dsPointCut() {}

    @Around ("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        HttpServletRequest request = sra.getRequest();
        String dataSource = request.getParameter("database");
        if (dataSource != null && DataSourceEnums.isValid(dataSource)) {
            DynamicDataSourceContextHolder.setDataSourceType(dataSource);
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
    }
}

Controller

DynamicController

@RestController
@RequestMapping(value = "/test")
public class DynamicController {

    @Resource
    IUserDao userDao;

    @GetMapping(value = "/findAll")
    public List<UserEntity> findAll() {
        return userDao.findAll();
    }
}

启动类

由于使用的是自定义的数据源配置,在启动时需要将Spring Boot 中扫描默认数据源的配置类排除掉,注解如下所示:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableTransactionManagement
public class DynamicApplication {
    public static void main(String[] args) {
        SpringApplication.run(DynamicApplication.class, args);
    }
}

演示

请求 /test/findAll 或者 /test/findAll?database=PRIMARY

不传默认是 PRIMARY 数据库

请求 /test/findAll?database=SECONDARY

  • 12
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值