Spring Boot 动态数据源

news2025/5/20 9:57:13

目录

前言

前置环境

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1968981.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Interceptor拦截器开发

因为1登录后的接口都需要token验证代码,会出现重复代码;2当前的接口不防刷,会被恶意攻击 所以在controller层增加请求拦截,如果你的token不合法,就不让你做后续的处理了 拦截器的作用是什么 作用: 1、对controller层代码的访问进行拦截,合法的请求,那此层代码就处理,反…

三好夫人是正规牌子吗?99%不知道的秘密:三好夫人竟成宠夫神器

三好夫人是正规品牌&#xff0c;是2023年在中国国家商标局注册的第30类商标。所属公司为苏州好夫人健康科技有限公司。 当下的社会&#xff0c;健康与爱情成为了现代人追求的两大宝藏。在众多养生品牌中&#xff0c;“三好夫人”如同一股清流&#xff0c;悄然间成为了许多家庭中…

【华为战报】2024年7月 HCIA-HCIP-HCIE考试,通过率100%

了解更多往期考试→点击 【考试战报】 HCIA 华为认证工程师 7月 微思 | HCIA 考试战报 HCIP 华为认证高级工程师 7月 微思 | HCIP 考试战报 HCIE 华为认证专家 7月 微思 | HCIE 考试战报 END 微思网络&#xff0c;始于2002年 专业IT认证培训22年&#xff0c;面向全国招生&a…

智能体互联网IoA架构核心思想解读

零、常用概念 智能体&#xff1a;融合了大模型能力&#xff0c;具有感知记忆、自主规划、调用工具、执行任务能力的AgentInternet of Agents &#xff08;IoA&#xff09;—— “智能体互联网”&#xff1a;Agent 之间互相发现&#xff0c;大规模连接、协作。 一、现状问题 生…

hadoop学习笔记4-mapreduce

5.MapReduce 5.1Linux中安装IDEA IDEA官网&#xff1a;https://www.jetbrains.com.cn/idea/ 点击右上角的下载 选择Linux进行下载压缩包 下载完成后找到压缩包并解压压缩包到当前目录下 tar -zxvf ideaIC-2024.1.4.tar.gz 运行idea ./idea.sh 5.2配置开发环境 1.mapreduce简…

公司运营数据分析大屏,非专业者也能轻松上手

在这个数据洪流的时代&#xff0c;企业的每一步发展都深深刻画在数字的轨迹之中。如何精准捕捉这些瞬息万变的信息&#xff0c;将其转化为推动企业前行的智慧力量&#xff1f;答案&#xff0c;或许就藏在一面高效、直观的公司运营数据分析大屏之中。 想象一下&#xff0c;当晨光…

软件工程-期末考试

目录 1.数据流图例题 2.工程网络 3.详细设计阶段图形工具 程序流程图&#xff0c;盒图 PAD图&#xff08;冒泡排序&#xff09; 判定树、判定表 4.合理地设计测试方案 5.能用jackson图表达问题的数据结构 6.能建立问题的对象模型--->&#xff08;类图&#xff09; …

shell脚本自动化部署

1、自动化部署DNS [rootweb ~]# vim dns.sh [roottomcat ~]# yum -y install bind-utils [roottomcat ~]# echo "nameserver 192.168.8.132" > /etc/resolv.conf [roottomcat ~]# nslookup www.a.com 2、自动化部署rsync [rootweb ~]# vim rsync.sh [rootweb ~]# …

Git安装流程以及如何将本地代码推送到新建的git仓库(IDEA操作简单易学)

Windows版本下载地址 Git - Downloading Package (git-scm.com) 根据自己电脑系统选择响应的版本 安装流程 1. 我安装的是 2.46.0 的版本&#xff0c; 点击next 2. 选择安装的文件夹&#xff0c;点击browse..进行选择&#xff0c;选择完点击next 3. 自定义组件&#xff0c;一…

系统架构师考点--软件架构的演化和维护

大家好。今天来总结一下软件架构的 演化和维护。这部分内容属于新增内容&#xff0c;之前未曾考过&#xff0c;可能会在论文中出现&#xff0c;在上午场客观题出现几率不大&#xff0c;可能会占分。 一、软件架构演化和定义 软件架构的演化和维护就是对架构进行修改和完善的过…

jmeter-beanshell学习15-输入日期,计算前后几天的日期

又遇到新问题了&#xff0c;想要根据获取的日期&#xff0c;计算出前面两天的日期。网上找了半天&#xff0c;全都是写获取当天日期&#xff0c;然后计算昨天的日期&#xff0c;照葫芦画瓢也没改出来想要的&#xff0c;最后求助了开发同学。 先放上网上获取当天&#xff0c;计…

使用Canal监听Binlog将数据发送到RocketMQ

文章目录 一、部署RocketMQ二、部署MySQL1、开启mysql的binlog写入功能2、创建一个有相关权限的mysql slave账号 三、部署 Canal1、修改conf/canal.properties配置文件2、修改conf/example/instance.properties配置文件 四、实际操作 一、部署RocketMQ win11 部署RocketMQ 和可…

u盘sd卡格式化怎么恢复数据:从绝望到希望的全面指南

在数字时代&#xff0c;U盘和SD卡已成为我们日常生活中不可或缺的数据存储设备。然而&#xff0c;一不留神的格式化操作&#xff0c;往往会导致重要数据的瞬间消失&#xff0c;让人倍感焦虑与无助。那么&#xff0c;面对这一突如其来的数据灾难&#xff0c;我们是否只能束手就擒…

大模型套壳祛魅:质疑套壳,理解套壳

过去的 2023 年是大模型元年&#xff0c;在国产大模型数量狂飙突进的同时——已经超过 200 个&#xff0c;「套壳」一直是萦绕在大模型头上的舆论阴云。 从年初到年末&#xff0c;从百度文心一言到零一万物&#xff0c;从字节跳动到谷歌 Gemini&#xff0c;各种「涉嫌套壳」的…

案例精选 | 南大港产业园区卫生计生管理服务中心日志系统建设方案

南大港产业园区&#xff0c;坐落于中国河北省沧州市东南部&#xff0c;是一个集工业生产、科技研发、商贸物流、生态居住等多功能于一体的国内知名综合性产业园区。作为区域经济发展的重要引擎&#xff0c;不仅承载着产业升级的重任&#xff0c;还肩负着提升公共服务水平、保障…

刷题了:513.找树左下角的值|112. 路径总和| 113. 路径总和ii|106.从中序与后序遍历序列构造二叉树|105.从前序与中序遍历序列构造二叉树

513.找树左下角的值 文章讲解:https://programmercarl.com/0513.%E6%89%BE%E6%A0%91%E5%B7%A6%E4%B8%8B%E8%A7%92%E7%9A%84%E5%80%BC.html 视频讲解:https://www.bilibili.com/video/BV1424y1Z7pn/?spm_id_from333.1007.top_right_bar_window_history.content.click&vd_s…

install第三方jar中包含私服依赖,导致项目无法构建

起因&#xff1a;新公司项目引入了一个发短信的jar包&#xff0c;我使用下列命令安装到本地库。 mvn install:install-file -DfileD:\workspace\\resources\WEB-INF\lib\xxxx.sdk.sms-0.0.1-SNAPSHOT.jar -DgroupIdxxxx.sdk.sms -DartifactIdxxxx.sdk.sms-0.0.1-SNAPSHOT -Dv…

Netty 必知必会(三)—— ByteBuf

Netty ByteBuf工作原理&#xff0c;和NIO里ByteBuffer区别&#xff1f; Java NIO 提供了ByteBuffer 作为它 的字节容器&#xff0c;但是这个类使⽤起来过于复杂&#xff0c;⽽且也有些繁琐。 ByteBuf是Netty框架中的一个关键类&#xff0c;专门设计来处理字节数据&#xff0c;…

大模型笔记4 长文本滑动窗口

Extractive QA参考: https://juejin.cn/post/7180925054559977533 https://huggingface.co/learn/nlp-course/en/chapter7/7 目录 滑动窗口例子(提取开始结束点任务) 滑动窗口代码实现 tokenize() 默认添加问题 每个滑窗添加标题和摘要 训练label跨滑窗情况token匹配 …

MyBatis开发: XML配置⽂件

前言 在IDEA的yml文件注释发现乱码 1.配置文件注释中文显示乱码 退出重进&#xff0c;发现不是乱码就成功 一.MyBatis XML配置⽂件 学习了注解的⽅式, 接下来我们学习XML的⽅式. 使⽤Mybatis的注解⽅式&#xff0c;主要是来完成⼀些简单的增删改查功能. 如果需要实现复杂的SQL…