SpringBoot如何配置动态数据源?原理+实战

news2024/9/27 23:32:06

若没空探究原理可直接跳转到“实现方式:注解+切面”目录

数据源切换方法

Spring对数据源的管理类似于策略模式,不懂策略模式也没关系,其实就是有一个全局的键值对,类型是Map<String, DataSource>。当JDBC操作数据库之时,会根据不同的key值选择不同的数据源。而这个key值可以放到方法的注解里。

所以切换数据源的思路就是让JDBC在获取数据源时根据key获取到要切换的数据源。

JDBC提供了AbstractRoutingDataSource抽象类,类名意思是数据源路由,该类提供了一个抽象方法determineCurrentLookupKey(),切换数据源时JDBC会调用这个方法获取数据源的key,所以只需要实现该方法,改变该方法中返回的key值即可。

源码解读

1.从类关系图中可以看出AbstractRoutingDataSource类实现了DataSource接口,后者有两个getConnection()方法,即获取DB连接的作用。

在这里插入图片描述

2.AbstractRoutingDataSource实现了这两个方法

在这里插入图片描述

其中determineTargetDataSource()方法的作用就是获取实际的数据源,其内部调用了determineCurrentLookupKey()方法,取到当前设定的key,通过key在上下文this.resolvedDataSources属性中尝试获取DataSource对象,这个对象即当前连接的数据源

在这里插入图片描述

3.那么this.resolvedDataSources在哪里维护呢? 继续在AbstractRoutingDataSource类里找,可以找到afterPropertiesSet()方法,这个方法是InitializingBean接口的,作用是在bean的所有属性设置完成后便会调用此方法。可以看到this.resolvedDataSources是从this.targetDataSources取的信息。

在这里插入图片描述

所以只需要改变this.targetDataSources,即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源

实现方式:注解+切面

别看步骤挺多,但其实很容易理解和使用

1.配置文件示例:

spring:
  datasource: 
    master: # 数据源1
      jdbc-url: jdbc:mysql://localhost:3306/master?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    db1: # 数据源1
      jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

2.创建数据源配置类

创建数据源配置类(我这里为了方便区分就为每一个数据源创建了一个配置类,当然也可以把所有的数据源配置在一个类里)

@Configuration
@EnableConfigurationProperties({MasterDataSourceProperties.class})
public class MasterDataSourceConfig {
    /**
    * 这个MasterDataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了
    **/
    @Autowired
    private MasterDataSourceProperties masterDataSourceProperties;

    @Bean
    @Primary
    public DataSource masterDataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(masterDataSourceProperties.getUrl());
        datasource.setUsername(masterDataSourceProperties.getUsername());
        datasource.setPassword(AESUtil.aesDecode(masterDataSourceProperties.getPassword()));
        datasource.setDriverClassName(masterDataSourceProperties.getDriverClassName());
        ......

        return datasource;
    }
}
@Configuration
@EnableConfigurationProperties({OdsDataSourceProperties.class})
public class DB1DataSourceConfig {
    /**
    * 这个DB1DataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了
    **/
    @Autowired
    private DB1DataSourceProperties dB1DataSourceProperties;

    @Bean
    public DataSource db1DataSource() {
        DruidDataSource datasource = new DruidDataSource();
        datasource.setUrl(dB1DataSourceProperties.getUrl());
        datasource.setUsername(dB1DataSourceProperties.getUsername());
        datasource.setPassword(AESUtil.aesDecode(dB1DataSourceProperties.getPassword()));
        datasource.setDriverClassName(dB1DataSourceProperties.getDriverClassName());
        ......

        return datasource;
    }
}

3.创建DynamicDataSource

创建自己的一个DynamicDataSource类(名字任意)继承AbstractRoutingDataSource,维护数据源,提供切换方法。

public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
    * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
    * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
    */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
    * 如果希望所有数据源在启动配置时就加载好,然后通过设置数据源Key值来切换数据,定制这个方法
    */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
    * 设置默认数据源
    *
    * @param defaultDataSource
    */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
    * 设置数据源
    *
    * @param dataSources
    */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}

4.创建数据源上下文处理器DynamicDataSourceContextHolder

创建数据源上下文处理器DynamicDataSourceContextHolder用以存储当前线程需要使用的数据源名称。

public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 master 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return "master";
        }
    };


    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 切换数据源
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置数据源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判断是否包含数据源
     *
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源keys
     *
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

5.创建数据源配置类DataSourceConfig

创建数据源配置类DataSourceConfig,将所有数据源注入到spring容器

@Configuration
@EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用
public class DataSourceConfig {

    @Autowired
    private MasterDataSourceConfig masterDataSourceConfig;

    @Autowired
    private DB1DataSourceConfig dB1DataSourceConfig;
    /**
     * 设置动态数据源为主数据源
     *
     * @return
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认指定的数据源
        dynamicDataSource.setDefaultDataSource(masterDataSourceConfig.masterDataSource());
        // 将数据源设置进map
        Map<Object, Object> dataSourceMap = new HashMap<>(8);
        dataSourceMap.put(DataSourceEnum.MASTER.toString(), masterDataSourceConfig.masterDataSource());
        dataSourceMap.put(DataSourceEnum.DB1.toString(), dB1DataSourceConfig.db1DataSource());
        // 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上
        dynamicDataSource.setDataSources(dataSourceMap);
        return dynamicDataSource;
    }
}

6.创建数据源类型枚举DataSourceEnum

public enum DataSourceEnum {

    /**默认类型*/
    MASTER,
    /**DB1类型*/
    DB1,
    ;
}

7.创建自定义注解@DataSource

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    /**
     * 数据源key值
     * @return
     */
    DataSourceEnum value();
}

8.创建切面DynamicDataSourceAspect

@Slf4j
@Aspect
@Order(-1)
@Component
public class DynamicDataSourceAspect {

    /**
     * 切换数据源
     *
     * @param point
     * @param dataSource
     */
    @Before("@annotation(dataSource))")
    public void switchDataSource(JoinPoint point, DataSource dataSource) {
        if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().toString())) {
            log.info("DataSource [{}] doesn't exist, use default DataSource", dataSource.value());
        } else {
            // 切换数据源
            DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().toString());
            log.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(),
                    point.getSignature());
        }
    }

    /**
     * 重置数据源
     *
     * @param point
     * @param dataSource
     */
    @After("@annotation(dataSource))")
    public void restoreDataSource(JoinPoint point, DataSource dataSource) {
        // 将数据源置为默认数据源
        DynamicDataSourceContextHolder.clearDataSourceKey();
        log.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
    }
}

如何使用

@Override
@DataSource(DataSourceEnum.DB1)
public Page<AuditTaskDto> queryAuditTask(AuditTaskQuery query) {
    Page<AuditTaskDto> page = baseMapper.queryAuditTask(query);
    return page;
}

如此就可以直接使用自定义的@DataSource注解来切换数据源啦~~~

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

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

相关文章

boot项目:程序包xxxx.xxxx.xxx不存在

你们好&#xff0c;我是金金金。 idea2021版本&#xff0c;在maven项目中运行程序的时候会出现java程序包不存在现象。(属于它的一个小bug) 场景 启动boot项目时报错如下 解决 清理缓存 重新加载所有maven项目 重新启动项目即可 测试 已测试&#xff0c;项目成功启动~ 编写有误…

代码规范 —— Redis 开发规范

优质博文&#xff1a;IT-BLOG-CN 一、开发规范 【1】弱依赖检查与线下确认&#xff1a;Redis必须是弱依赖&#xff0c;即Redis宕机不影响业务。包括超时检查。 【2】是否当存储使用检查&#xff1a;Redis不能作为存储设备来使用&#xff0c;只能作为缓存或状态等场景来使用。…

【Mudo库】实战项目之简要介绍

文章目录 前言一、效果演示二、模块1. 介绍2. 服务器模块3. 应用层模块 尾序 前言 各位C友们&#xff0c;好久不见&#xff0c;最近一个月在搞项目&#xff0c;算是半摆半学的状态吧&#xff0c;博客断更了一段时间&#xff0c;现在项目搞完了&#xff0c;博客之后也会慢慢更新…

机器学习周报(8.12-8.18)

文章目录 摘要Abstract1.Transformer的结构1.1 序列到序列&#xff08;Seq2seq&#xff09;的模型1.2 Transformer 结构1.2.1 Transformer 编码器&#xff08;Encoder&#xff09;1.2.2 Transformer解码器&#xff08;Decoder&#xff09;1.2.3 编码器-解码器注意力&#xff08…

Java语言程序设计——篇十四(2)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

c++11(二)

一、右值引用 1、区分左值和右值 语法定义&#xff0c;左值可以取地址&#xff0c;右值无法取地址&#xff08;右值肯定有地址&#xff0c;但是为了和左值区分&#xff0c;语法上不让取地址&#xff09; 左值&#xff1a;一个表示数据的表达式&#xff08;变量名或解引用指针…

如何配置ESXI主机的IP地址管理

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f427;Linux基础知识(初学)&#xff1a;点击&#xff01; &#x1f427;Linux高级管理防护和群集专栏&#xff1a;点击&#xff01; &#x1f510;Linux中firewalld防火墙&#xff1a;点击&#xff01; ⏰️创作…

通过python脚本查询自己阿里云账号里的某个域名的A记录解析情况,以及测拨,用于排查未使用的解析

安装sdk pip install aliyun-python-sdk-alidns代码全文 import json import requests from aliyunsdkcore.client import AcsClient from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest# 替换为你的阿里云 AccessKey ID 和 AccessKey Secret acce…

栈与队列 - 逆波兰表达式求值

150. 逆波兰表达式求值 方法一&#xff1a;栈 /*** param {string[]} tokens* return {number}*/ var evalRPN function(tokens) {const stack [];for (const token of tokens) {if (isNaN(Number(token))) { // 非数字const n2 stack.pop(); // 出栈两个数字const n1 s…

假期作业--数据结构

1、顺序表实现学生管理系统&#xff08;参照顺序表技能&#xff09;写出菜单界面switch选择&#xff0c;功能1创建顺序表&#xff08;堆区&#xff09;&#xff0c;2录入学生信息&#xff0c;3插入一个学生信息&#xff0c;4删除一个学生信息&#xff0c;5按照位置修改一个学生…

javaEE中自定义注解以及注解的解析

注解&#xff1a; 就是java代码里的特殊标记&#xff0c;比如Override、Test,作用是&#xff1a;让其它程序根据注解信息来决定怎么执行程序。 自定义注解&#xff1a;自己定义注解 Public interface 注解名称{ Public 属性类型 属性名&#xff08;&#xff09; default 默认…

写字楼/办公室为什么要建设智慧公厕?有哪些价值?@卓振思众

智慧公厕是指利用先进技术和设备对公共厕所进行智能化管理的系统。这些技术包括物联网&#xff08;IoT&#xff09;、传感器技术、大数据分析和自动化系统等。【卓振思众】智慧公厕不仅提升了公厕的使用体验&#xff0c;还实现了更高效的管理和维护。 写字楼/办公室智慧公厕的定…

揭秘RAG与大模型对接:深入探讨9大隐藏挑战

前一段时间&#xff0c;各个大模型在争斗&#xff1a;谁能携带更长、更大的上下文 Prompt&#xff0c;比如 Kimi 说 200 万字&#xff0c;阿里通义千问又说自己能达 1000 万字&#xff1b;大家都知道 Prompt 很重要&#xff0c;但是 RAG 和 长的上下文文本携带 是两个不同的技术…

mac查看jdk安装目录

打开终端&#xff0c;直接输入命令&#xff1a; /usr/libexec/java_home终端即会输出jdk的安装目录&#xff1a;

8.17日学习打卡---Spring Cloud Alibaba(四)

8.17日学习打卡 目录&#xff1a; 8.17日学习打卡分布式流量防护什么是服务雪崩解决方案内部异常治理外部流量控制 SentinelSentinel 基本概念Sentinel 的主要特性Sentinel 是如何工作的Sentinel 与 Hystrix、resilience4j 的对比安装Sentinel控制台将应用接入Sentinel流量控制…

python使用flask实现自动根据url寻找对应目录/文件/方法,实现动态路由

例如访问:/user/index/index_config 则自动访问/user 目录里 index.py文件里的 index_config 方法 实现代码 from flask import Flask,jsonifyapp Flask(__name__)def reg_func(code, data, msg, debugNone, showFalse):return jsonify({code: code,data: data,msg: msg,time…

iPhone照片怎么导入电脑?一键导入毫不费力

随着智能手机的普及&#xff0c;我们越来越依赖手机来记录生活的点点滴滴。iPhone作为其中的佼佼者&#xff0c;其高质量的摄像头为用户捕捉了无数珍贵瞬间。然而&#xff0c;随着照片数量的增多&#xff0c;手机存储空间可能会变得捉襟见肘&#xff0c;此时将照片导入电脑既能…

UniAD_面向规划的自动驾驶

Planning-oriented Autonomous Driving 面向规划的自动驾驶 https://github.com/OpenDriveLab/UniAD Abstract Modern autonomous driving system is characterized as modular tasks in sequential order, i.e., perception, prediction, and planning. In order to perfor…

Python 如何创建和管理虚拟环境?

Python虚拟环境是一个独立的运行环境&#xff0c;能够与系统的全局Python环境相隔离。它允许你在不影响系统其他项目的前提下&#xff0c;为每个项目创建独立的Python环境&#xff0c;并在该环境中安装特定版本的包和依赖项。这在开发多个项目时非常有用&#xff0c;尤其是当这…

EmguCV学习笔记 VB.Net 4.1 颜色变换

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 教程VB.net版本请访问&#xff1a;EmguCV学习笔记 VB.Net 目录-CSDN博客 教程C#版本请访问&#xff1a;EmguCV学习笔记 C# 目录-CSD…