首页 > 其他分享 >在 SpringBoot 项目中多数据源切换

在 SpringBoot 项目中多数据源切换

时间:2024-02-08 20:22:18浏览次数:20  
标签:return SpringBoot 数据源 private class dataSource public 中多

使用dynamic-datasource-spring-boot-starter库

添加依赖

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.4.1</version>
</dependency>

添加配置

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver

代码使用

@Service
@DS("master")
public class UserService {

  @Autowired
  private UserRepository userRepo;

  public void queryAndUpdate(List<Integer> deptIds) {
    List<User> userList = userRepo.findAll();
    System.out.println(userList.size());
  }
}

通过@DS注解来切换数据源,使用还是很简单的。

原理分析

  1. 自动配置类 DynamicDataSourceAutoConfiguration 会配置 DynamicDataSourceAnnotationInterceptor 拦截器及 DynamicRoutingDataSource 动态数据源类。
  2. DynamicDataSourceAnnotationInterceptor 拦截@DS注解,获取注解配置的数据源名称,如 master,存储到 DynamicDataSourceContextHolder 中,内部是一个 ThreadLocal。
  3. DynamicRoutingDataSource 在对象初始化时(afterPropertiesSet方法)会根据 yml 配置创建多个数据源 dataSourceMap,包含 master 和 slave,在 getConnection() 时,从 DynamicDataSourceContextHolder 中取出配置的数据源名称,根据名称从 dataSourceMap 中获取到真正的 DataSource。

自己实现一个简单的动态切换库

添加配置

和上面配置保持一致

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://ip:port/testdb?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql:/ip:port/testdb2?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: xxx
          driver-class-name: com.mysql.cj.jdbc.Driver

定义实体类

用来接收 yml 配置

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public class MasterDataSourceProperty {

    private String url;
    private String username;
    private String password;
    private String driverClassName;

}
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
public class SlaveDataSourceProperty {

    private String url;
    private String username;
    private String password;
    private String driverClassName;

}

定义ContextHolder

public class DBContextHolder {

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

    /**
     * 绑定当前线程数据源路由的key
     * 使用完成后必须调用removeRouteKey()方法删除
     * @param dataSource
     */
    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    /**
     * 获取当前线程的数据源路由的key
     * @return
     */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 删除与当前线程绑定的数据源路由的key
     */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

存储拦截器获取到的数据源名称,供后续使用

定义数据源配置

@Configuration
@Slf4j
public class DataSourceConfig {

    @Value("${spring.datasource.dynamic.primary}")
    private String defaultTargetDataSource;

    /**
     * 主数据源
     *
     * @return
     */
    @Bean("masterDataSource")
    public DataSource masterDataSource(MasterDataSourceProperty dataSourceProperty) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperty.getUrl());
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
        return dataSource;
    }

    /**
     * 从数据源
     *
     * @return
     */
    @Bean("slaveDataSource")
    public DataSource slaveDataSource(SlaveDataSourceProperty dataSourceProperty) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(dataSourceProperty.getUrl());
        dataSource.setUsername(dataSourceProperty.getUsername());
        dataSource.setPassword(dataSourceProperty.getPassword());
        dataSource.setDriverClassName(dataSourceProperty.getDriverClassName());
        return dataSource;
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        return new DynamicDataSource(defaultTargetDataSource, targetDataSources);
    }

    public static class DynamicDataSource extends AbstractRoutingDataSource {

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource(String defaultTargetDataSource, Map<Object, Object> targetDataSources) {
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
        }

    }
}

此配置类在低版本的 SpringBoot 项目中会报错

 * The dependencies of some of the beans in the application context form a cycle:
 *
 *    masterUserService
 *       ↓
 *    userMapper defined in file [C:\D-myfiles\java\code_resp\gitee\spring_ds\target\classes\com\imooc\ds\service\UserMapper.class]
 *       ↓
 *    sqlSessionFactory defined in class path resource [com/baomidou/mybatisplus/autoconfigure/MybatisPlusAutoConfiguration.class]
 * ┌─────┐
 * |  dynamicDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
 * ↑     ↓
 * |  masterDataSource defined in class path resource [com/imooc/ds/custom/DataSourceConfig.class]
 * ↑     ↓
 * |  org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker
 * └─────┘

具体原因为:

  1. DynamicDataSource 在创建 Bean 时依赖 masterDataSource
  2. 创建 masterDataSource 的 Bean 后,会触发 DataSourceInitializerPostProcessor 处理器的 postProcessAfterInitialization()方法,此方法会创建 DataSourceInitializerInvoker 的 Bean 实例
  3. DataSourceInitializerInvoker 又会依赖 DataSource的 Bean 实例,这里就会依赖 DynamicDataSource(因为我们设置了@Primary注解),到这里就形成了循环依赖。

Spring 框架不支持构造器注入方式的循环依赖,但支持属性注入的循环依赖,具体原理参考 Spring源码分析之循环引用 ,所以我们的解决方案就是将构造器注入改为属性注入,具体代码见下文。

定义数据源配置(解决循环依赖)

@Configuration
@Slf4j
public class DataSourceConfig2 {

    @Autowired
    private MasterDataSourceProperty masterDataSourceProperty;
    @Autowired
    private SlaveDataSourceProperty slaveDataSourceProperty;

    /**
     * 主数据源
     *
     * @return
     */
    @Bean("masterDataSource")
    public DataSource masterDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
        dataSource.setUsername(masterDataSourceProperty.getUsername());
        dataSource.setPassword(masterDataSourceProperty.getPassword());
        dataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());
        return dataSource;
    }

    /**
     * 从数据源
     *
     * @return
     */
    @Bean("slaveDataSource")
    public DataSource slaveDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
        dataSource.setUsername(slaveDataSourceProperty.getUsername());
        dataSource.setPassword(slaveDataSourceProperty.getPassword());
        dataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());
        return dataSource;
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource2 dataSource() {
        return new DynamicDataSource2();
    }

    /**
     * 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
     */
    public static class DynamicDataSource2 extends AbstractRoutingDataSource {

        @Autowired
        @Qualifier("masterDataSource")
        private DataSource masterDataSource;
        @Autowired
        @Qualifier("slaveDataSource")
        private DataSource slaveDataSource;
        @Value("${spring.datasource.dynamic.primary}")
        private String defaultTargetDataSource;

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource2() {
        }

        @Override
        public void afterPropertiesSet() {
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("master", masterDataSource);
            targetDataSources.put("slave", slaveDataSource);
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }
    }
}

通过@Autowired注解在 Bean初始化时注入,而不是在 Bean 实例化(创建对象)时就注入,就可以解决循环依赖的问题了。或者下面这种方式也可以,还更好理解一些。

@Configuration
@Slf4j
public class DataSourceConfig3 {

    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource3 dataSource() {
        return new DynamicDataSource3();
    }

    /**
     * 正确的配置,在DataSourceInitializerPostProcessor处理之后再初始化数据
     */
    public static class DynamicDataSource3 extends AbstractRoutingDataSource {

        @Value("${spring.datasource.dynamic.primary}")
        private String defaultTargetDataSource;

        @Autowired
        private MasterDataSourceProperty masterDataSourceProperty;
        @Autowired
        private SlaveDataSourceProperty slaveDataSourceProperty;

        /**
         * 实现数据源切换要扩展的方法,
         * 通过返回值获取指定的数据源
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DBContextHolder.getDataSource();
        }

        public DynamicDataSource3() {
        }

        @Override
        public void afterPropertiesSet() {
            Map<Object, Object> targetDataSources = createTargetDataSources();
            //设置默认数据源
            super.setDefaultTargetDataSource(targetDataSources.get(defaultTargetDataSource));
            //设置数据源列表
            super.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }

        private Map<Object, Object> createTargetDataSources() {
            HikariDataSource masterDataSource = new HikariDataSource();
            masterDataSource.setJdbcUrl(masterDataSourceProperty.getUrl());
            masterDataSource.setUsername(masterDataSourceProperty.getUsername());
            masterDataSource.setPassword(masterDataSourceProperty.getPassword());
            masterDataSource.setDriverClassName(masterDataSourceProperty.getDriverClassName());

            HikariDataSource slaveDataSource = new HikariDataSource();
            slaveDataSource.setJdbcUrl(slaveDataSourceProperty.getUrl());
            slaveDataSource.setUsername(slaveDataSourceProperty.getUsername());
            slaveDataSource.setPassword(slaveDataSourceProperty.getPassword());
            slaveDataSource.setDriverClassName(slaveDataSourceProperty.getDriverClassName());

            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put("master", masterDataSource);
            targetDataSources.put("slave", slaveDataSource);
            return targetDataSources;
        }
    }
}

dynamic-datasource-spring-boot-starter 库中就是这种方式。

定义拦截器及数据源切换的注解

@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
    String value();
}
@Slf4j
@Aspect
@Component
public class DataSourceAspect {

    /**
     * 在类上和方法上都会拦截到
     */
    @Pointcut(value = "@annotation(com.imooc.ds.custom.TargetDataSource) " +
            "|| @within(com.imooc.ds.custom.TargetDataSource)")
    public void dataSourcePointCut() {
    }

    @Before("dataSourcePointCut()")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        String tragetDataSource = getTragetDataSource(method);
        DBContextHolder.setDataSource(tragetDataSource);
        log.info("当前线程名称:{},当前数据源:{}", Thread.currentThread().getName(), tragetDataSource);
    }

    private String getTragetDataSource(Method method) {
        TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
        if (Objects.nonNull(annotation)) {
            return annotation.value();
        }
        Class<?> declaringClass = method.getDeclaringClass();
        annotation = declaringClass.getAnnotation(TargetDataSource.class);
        if (Objects.nonNull(annotation)) {
            return annotation.value();
        }
        return null;
    }

    @After("dataSourcePointCut()")
    public void doAfterReturning() {
        DBContextHolder.clearDataSource();
    }
}

拦截类或者方法上的@TargetDataSource 注解,方法优先级更高。

参考

dynamic-datasource-gitee仓库

标签:return,SpringBoot,数据源,private,class,dataSource,public,中多
From: https://www.cnblogs.com/strongmore/p/17998454

相关文章

  • SpringBoot脚手架使用
    介绍脚手架可以帮助我们快速创建SpringBoot项目。Spring提供的脚手架页面地址,核心为https://github.com/spring-io/initializr这个项目,https://github.com/spring-io/start.spring.io这个项目在此基础上提供了一些额外配置,并提供了前端页面。内部是通过https://start.spri......
  • SpringBoot简介
    1、为什么有SpringBoot?J2EE笨重的开发、繁多的配置、低下的开发效率、复杂的部署流程、第三方技术集成难度大。2、SpringBoot是什么?是一个一站式整合所有应用框架的框架;并且完美整合Spring技术栈。SpringBoot来简化开发,约定大于配置,去繁从简,justrun就能创建一个......
  • 【Spring】SpringBoot3+SpringBatch5.xの構築
    ■概要  ■POMのXMLの設定<?xmlversion="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation=&qu......
  • SpringBoot 优雅实现超大文件上传,通用方案
    文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法,毕竟很少有人会忍受,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。那有没有比较好的上传......
  • Springboot和Vue(2或者3都行)实现Twitter授权登录,并获取用户公开信息-OAuth1.0。
    第一步先申请twitter开发者账号,创建App,我这里没有创建app,当时好像是默认有一个app,twitter官方说,创建一个app需要先删除一个app,我是没有充钱的,不知道充钱和免费使用接口的是不是一样的。第二步在生成CustomerKey以及CustomeSecret,我之后会用到这两个,这写密钥一生成永久有效,除非......
  • SpringBoot集成Flink-CDC 采集PostgreSQL变更数据发布到Kafka
    (之前写了一个flink-cdc同步数据的博客,发布在某N,最近代码开源了,直接复制过来了,懒得重新写了,将就着看下吧)最近做的一个项目,使用的是pg数据库,公司没有成熟的DCD组件,为了实现数据变更消息发布的功能,我使用SpringBoot集成Flink-CDC采集PostgreSQL变更数据发布到Kafka。 一、业务......
  • SpringBoot使用Validation框架手动校验对象是否符合规则
      在springboot项目中引入<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency> 伪代码importlombok.Data;import......
  • IntelliJ 跨数据源导入数据迁移
    有这么一个需求,我们需要把服务器上一个测试表中的输入导入到本地的数据库中。IntelliJ已经设置了2个数据源。我们可以通过IntelliJ的数据迁移工具在2个数据源中进行迁移。找到需要导出的表首先我们需要找到需要导出的表,然后从表中选中导出。   选择拷贝表......
  • springboot集成easypoi导出多sheet页
    pom文件<dependency> <groupId>cn.afterturn</groupId> <artifactId>easypoi-base</artifactId> <version>4.1.0</version></dependency>导出模板:编辑后端代码示例:/***导出加油卡进便利店大额审批列表*@throwsIOException......
  • SpringBoot的maven插件生成可以直接启动的jar
    简单使用<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration&g......