SpringBoot整合多数据源,并支持动态新增与切换

SpringBoot整合多数据源,并支持动态新增与切换

一、概述

在项目的开发过程中,遇到了需要从数据库中动态查询新的数据源信息并切换到该数据源做相应的查询操作,这样就产生了动态切换数据源的场景。为了能够灵活地指定具体的数据库,本文基于注解和AOP的方法实现多数据源自动切换。在使用过程中,只需要添加注解就可以使用,简单方便。(代码获取方式:见文章底部(开箱即用))

二、构建核心代码

2.1、AbstractRoutingDataSource构建


package com.wonders.dynamic;import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;/*** @Description: TODO:抽象类AbstractRoutingDataSource,实现动态数据源切换* @Author: yyalin* @CreateDate: 2023/7/16 14:40* @Version: V1.0*/
public abstract class AbstractRoutingDataSource extends AbstractDataSourceimplements InitializingBean {//目标数据源map集合,存储将要切换的多数据源bean信息@Nullableprivate Map<Object, Object> targetDataSources;//未指定数据源时的默认数据源对象@Nullableprivate Object defaultTargetDataSource;private boolean lenientFallback = true;//数据源查找接口,通过该接口的getDataSource(String dataSourceName)获取数据源信息private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();//解析targetDataSources之后的DataSource的map集合@Nullableprivate Map<Object, DataSource> resolvedDataSources;@Nullableprivate DataSource resolvedDefaultDataSource;//将targetDataSources的内容转化一下放到resolvedDataSources中,将defaultTargetDataSource转为DataSource赋值给resolvedDefaultDataSourcepublic void afterPropertiesSet() {//如果目标数据源为空,会抛出异常,在系统配置时应至少传入一个数据源if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");} else {//初始化resolvedDataSources的大小this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());//遍历目标数据源信息map集合,对其中的key,value进行解析this.targetDataSources.forEach((key, value) -> {//resolveSpecifiedLookupKey方法没有做任何处理,只是将key继续返回Object lookupKey = this.resolveSpecifiedLookupKey(key);//将目标数据源map集合中的value值(德鲁伊数据源信息)转为DataSource类型DataSource dataSource = this.resolveSpecifiedDataSource(value);//将解析之后的key,value放入resolvedDataSources集合中this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {//将默认目标数据源信息解析并赋值给resolvedDefaultDataSourcethis.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);}}}protected Object resolveSpecifiedLookupKey(Object lookupKey) {return lookupKey;}protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {if (dataSource instanceof DataSource) {return (DataSource)dataSource;} else if (dataSource instanceof String) {return this.dataSourceLookup.getDataSource((String)dataSource);} else {throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);}}//因为AbstractRoutingDataSource继承AbstractDataSource,而AbstractDataSource实现了DataSource接口,所有存在获取数据源连接的方法public Connection getConnection() throws SQLException {return this.determineTargetDataSource().getConnection();}public Connection getConnection(String username, String password) throws SQLException {return this.determineTargetDataSource().getConnection(username, password);}protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");//调用实现类中重写的determineCurrentLookupKey方法拿到当前线程要使用的数据源的名称Object lookupKey = this.determineCurrentLookupKey();//去解析之后的数据源信息集合中查询该数据源是否存在,如果没有拿到则使用默认数据源resolvedDefaultDataSourceDataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");} else {return dataSource;}}@Nullableprotected abstract Object determineCurrentLookupKey();
}

**2.2、**DynamicDataSource类

/*** @Description: TODO:动态数据源* @Author: yyalin* @CreateDate: 2023/7/16 14:46* @Version: V1.0*/
/**** 调用AddDefineDataSource组件的addDefineDynamicDataSource()方法,获取原来targetdatasources的map,* 并将新的数据源信息添加到map中,并替换targetdatasources中的map* 切换数据源时可以使用@DataSource(value = "数据源名称"),或者DynamicDataSourceContextHolder.setContextKey("数据源名称")*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DynamicDataSource extends AbstractRoutingDataSource {//备份所有数据源信息,private Map<Object, Object> defineTargetDataSources;/*** 决定当前线程使用哪个数据源*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceHolder.getDynamicDataSourceKey();}}

2.3、DynamicDataSourceHolder


/*** @Description: TODO:数据源切换处理* DynamicDataSourceHolder类主要是设置当前线程的数据源名称,* 移除数据源名称,以及获取当前数据源的名称,便于动态切换* @Author: yyalin* @CreateDate: 2023/7/16 14:51* @Version: V1.0*/
@Slf4j
public class DynamicDataSourceHolder {/*** 保存动态数据源名称*/private static final ThreadLocal<String> DYNAMIC_DATASOURCE_KEY = new ThreadLocal<>();/*** 设置/切换数据源,决定当前线程使用哪个数据源*/public static void setDynamicDataSourceKey(String key){log.info("数据源切换为:{}",key);DYNAMIC_DATASOURCE_KEY.set(key);}/*** 获取动态数据源名称,默认使用mater数据源*/public static String getDynamicDataSourceKey(){String key = DYNAMIC_DATASOURCE_KEY.get();return key == null ? DbsConstant.mysql_db_01 : key;}/*** 移除当前数据源*/public static void removeDynamicDataSourceKey(){log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());DYNAMIC_DATASOURCE_KEY.remove();}}

2.4、数据源工具类

/*** @Description: TODO:数据源工具类* @Author: yyalin* @CreateDate: 2023/7/16 15:00* @Version: V1.0*/
@Slf4j
@Component
public class DataSourceUtils {@ResourceDynamicDataSource dynamicDataSource;/*** @Description: 根据传递的数据源信息测试数据库连接* @Author zhangyu*/public DruidDataSource createDataSourceConnection(DataSourceInfo dataSourceInfo) {DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setUrl(dataSourceInfo.getUrl());druidDataSource.setUsername(dataSourceInfo.getUserName());druidDataSource.setPassword(dataSourceInfo.getPassword());druidDataSource.setDriverClassName(dataSourceInfo.getDriverClassName());druidDataSource.setBreakAfterAcquireFailure(true);druidDataSource.setConnectionErrorRetryAttempts(0);try {druidDataSource.getConnection(2000);log.info("数据源连接成功");return druidDataSource;} catch (SQLException throwables) {log.error("数据源 {} 连接失败,用户名:{},密码 {}",dataSourceInfo.getUrl(),dataSourceInfo.getUserName(),dataSourceInfo.getPassword());return null;}}/*** @Description: 将新增的数据源加入到备份数据源map中* @Author zhangyu*/public void addDefineDynamicDataSource(DruidDataSource druidDataSource, String dataSourceName){Map<Object, Object> defineTargetDataSources = dynamicDataSource.getDefineTargetDataSources();defineTargetDataSources.put(dataSourceName, druidDataSource);dynamicDataSource.setTargetDataSources(defineTargetDataSources);dynamicDataSource.afterPropertiesSet();}

2.5、DynamicDataSourceConfig

/*** @Description: TODO:数据源信息配置类,读取数据源配置信息并注册成bean。* @Author: yyalin* @CreateDate: 2023/7/16 14:54* @Version: V1.0*/
@Configuration
@MapperScan("com.wonders.mapper")
@Slf4j
public class DynamicDataSourceConfig {@Bean(name = DbsConstant.mysql_db_01)@ConfigurationProperties("spring.datasource.mysqldb01")public DataSource masterDataSource() {log.info("数据源切换为:{}",DbsConstant.mysql_db_01);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean(name = DbsConstant.mysql_db_02)@ConfigurationProperties("spring.datasource.mysqldb02")public DataSource slaveDataSource() {log.info("数据源切换为:{}",DbsConstant.mysql_db_02);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean(name = DbsConstant.oracle_db_01)@ConfigurationProperties("spring.datasource.oracledb01")public DataSource oracleDataSource() {log.info("数据源切换为oracle:{}",DbsConstant.oracle_db_01);DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean@Primarypublic DynamicDataSource dynamicDataSource(){Map<Object, Object> dataSourceMap = new HashMap<>(3);dataSourceMap.put(DbsConstant.mysql_db_01,masterDataSource());dataSourceMap.put(DbsConstant.mysql_db_02,slaveDataSource());dataSourceMap.put(DbsConstant.oracle_db_01,oracleDataSource());//设置动态数据源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setDefaultTargetDataSource(masterDataSource());dynamicDataSource.setTargetDataSources(dataSourceMap);//将数据源信息备份在defineTargetDataSources中dynamicDataSource.setDefineTargetDataSources(dataSourceMap);return dynamicDataSource;}}

三、测试代码


/*** @Description: TODO* @Author: yyalin* @CreateDate: 2023/7/16 15:02* @Version: V1.0*/
@Slf4j
@Api(tags="动态切换多数据源测试")
@RestController
public class TestController {@ResourceDataSourceUtils dataSourceUtils;@Autowiredprivate StudentMapper studentMapper;@ApiOperation(value="动态切换多数据源测试", notes="test")@GetMapping("/test")public Map<String, Object> dynamicDataSourceTest(String id){Map<String, Object> map = new HashMap<>();//1、默认库中查询数据Student student=studentMapper.selectById(id);map.put("1、默认库中查询到的数据",student);//2、指定库中查询的数据DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);Student student02=studentMapper.selectById(id);map.put("2、指定库中查询的数据",student02);//3、从数据库获取连接信息,然后获取数据//模拟从数据库中获取的连接DataSourceInfo dataSourceInfo = new DataSourceInfo("jdbc:mysql://127.0.0.1:3308/test02?useUnicode=true&characterEncoding=utf-8&useSSL=false","root","root","mysqldb03","com.mysql.cj.jdbc.Driver");map.put("dataSource",dataSourceInfo);log.info("数据源信息:{}",dataSourceInfo);//测试数据源连接DruidDataSource druidDataSource = dataSourceUtils.createDataSourceConnection(dataSourceInfo);if (Objects.nonNull(druidDataSource)){//将新的数据源连接添加到目标数据源map中dataSourceUtils.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());//设置当前线程数据源名称-----代码形式DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());//在新的数据源中查询用户信息Student student03=studentMapper.selectById(id);map.put("3、动态数据源查询的数据",student03);//关闭数据源连接druidDataSource.close();}//4、指定oracle库中查询的数据DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.oracle_db_01);Student student04=studentMapper.selectById(id);map.put("4、指定oracle库中查询的数据",student04);return map;}
}

测试结果如下:

从结果中可以明显的看出,通过切换不同的数据源,可以从不同的库中获取不同的数据,包括:常见库Mysql、oracle、sqlserver等数据库相互切换。也可以从数据库的某张表中获取连接信息,实现*动态切换数据库。*

图片

四、使用注解方式切换数据源

从上述TestController 中代码不难看出,若要想切换数据源需要在mapper调用之前调用:

DynamicDataSourceHolder.setDynamicDataSourceKey(DbsConstant.mysql_db_02);

不够简洁优雅,所以下面推荐使用注解的方式来动态进行数据源的切换。

4.1、创建注解类DataSource

/*** @Description: TODO:自定义多数据源切换注解* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准* @Author: yyalin* @CreateDate: 2023/7/17 14:00* @Version: V1.0*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {//切换数据源名称,默认mysql_db_01public String value() default DbsConstant.mysql_db_01;
}

**4.2、**创建切面DataSourceAspect类

/*** @Description: TODO:创建切面DataSourceAspect类* @Author: yyalin* @CreateDate: 2023/7/17 14:03* @Version: V1.0*/
@Aspect
@Component
public class DataSourceAspect {// 设置DataSource注解的切点表达式@Pointcut("@annotation(com.wonders.dynamic.DataSource)")public void dynamicDataSourcePointCut(){}//环绕通知@Around("dynamicDataSourcePointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable{String key = getDefineAnnotation(joinPoint).value();DynamicDataSourceHolder.setDynamicDataSourceKey(key);try {return joinPoint.proceed();} finally {DynamicDataSourceHolder.removeDynamicDataSourceKey();}}/*** 功能描述:先判断方法的注解,后判断类的注解,以方法的注解为准* @MethodName: getDefineAnnotation* @MethodParam: [joinPoint]* @Return: com.wonders.dynamic.DataSource* @Author: yyalin* @CreateDate: 2023/7/17 14:09*/private DataSource getDefineAnnotation(ProceedingJoinPoint joinPoint){MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();DataSource dataSourceAnnotation = methodSignature.getMethod().getAnnotation(DataSource.class);if (Objects.nonNull(methodSignature)) {return dataSourceAnnotation;} else {Class<?> dsClass = joinPoint.getTarget().getClass();return dsClass.getAnnotation(DataSource.class);}}
}

4.3、进行数据源切换

//@Mapper 与 启动类的@MapperScan({"com.example.demo.mapper"}) 二选一即可
@Repository
public interface StudentMapper extends BaseMapper<Student> {/*** 功能描述:在mysql_db_01中查询数据* @MethodName: findStudentById* @MethodParam: [id]* @Return: com.wonders.entity.Student* @Author: yyalin* @CreateDate: 2023/7/17 14:20*/@DataSource(value = DbsConstant.oracle_db_01)Student findStudentById(String id);
}

或在service层

@Service
public class StudentServiceImpl implements StudentService{@Autowiredprivate StudentMapper studentMapper;//注解加在实现层才能生效@DataSource(value = DbsConstant.mysql_db_01)@Overridepublic Student findStudentById(String id) {return studentMapper.selectById(id);}
}

4.4、测试效果


@ApiOperation(value="使用注解方式动态切换多数据源", notes="test02")@GetMapping("/test02")public Student test02(String id){Student student=studentMapper.findStudentById(id);return student;}

–结果如下:

图片

五、功能点

1、使用注解的方式来动态进行数据源的切换;

2、支持动态新增新的数据源;

3、支持oracle\mysql等常见数据库切换。

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

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

相关文章

【深度学习:SENet】信道注意力和挤压激励网络(SENet):图像识别的新突破

【深度学习&#xff1a;SENet】信道注意力和挤压激励网络&#xff08;SENet&#xff09;&#xff1a;图像识别的新突破 为什么有效如何实现工作原理应用案例 挤压和激励网络&#xff08;SENets&#xff09;为卷积神经网络&#xff08;CNN&#xff09;引入了一个新的构建模块&am…

2024年1月1日答案

a)i. V B B V C C 16 V V_{BB} V_{CC} 16V VBB​VCC​16V R t h R B R E R B R E 10 k Ω 3 k Ω 10 k Ω 3 k Ω ≈ 2.31 k Ω R_{th} \frac{R_B \times R_E}{R_B R_E} \frac{10k\Omega \times 3k\Omega}{10k\Omega 3k\Omega} \approx 2.31k\Omega Rth​RB​R…

Web网页开发-盒模型-笔记

1.CSS的三种显示方式 (1)块级元素:标签所占区域默认为一行 特点&#xff1a;一行一个 可设宽高 (2)行内元素&#xff1a;标签所占区域由内容顶开&#xff0c;行内元素无法使用text-align 特点&#xff1a;一行多个 不可设宽高&#xff0c;margin上下和padding上下都不能改变位…

克服幻觉:提升语言模型在自然语言处理中的准确性与可靠性

随着语言模型&#xff08;LLM&#xff09;在自然语言处理&#xff08;NLP&#xff09;中的应用日益普及&#xff0c;它们在文本生成、机器翻译、情感分析等许多任务中展现出惊人的能力。然而&#xff0c;这些模型也常常显示出一个被称作“幻觉”&#xff08;hallucination&…

扫拖一体机哪个牌子好用?2024旗舰洗地机总结

近年来&#xff0c;家庭清洁的方式发生了翻天覆地的变化。在这场前所未有的洗地机创新浪潮中&#xff0c;消费者们迎来了更为便捷高效的家庭清洁解决方案。然而&#xff0c;随着市场竞争的激烈&#xff0c;面对众多品牌和型号的家用洗地机&#xff0c;究竟哪款扫拖一体机好用呢…

实现区域地图散点图效果,vue+echart地图+散点图

需求&#xff1a;根据后端返回的定位坐标数据实现定位渲染 1.效果图 2.准备工作,在main.js和index.js文件中添加以下内容 main.js app.use(BaiduMap, {// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */ak: sRDDfAKpCSG5iF1rvwph4Q95M…

在spring boot中集成druid的数据监控页面

参考例子见这里&#xff0c;亲测有效&#xff01; 核心就是要加入下面的配置code Configuration public class DruidDataMonitorConfig {Beanpublic ServletRegistrationBean statViewServlet() {ServletRegistrationBean bean new ServletRegistrationBean(new StatViewServ…

使用 go-elasticsearch v8 基本请求

使用 go-elasticsearch 请求示例 你可以通过参考Go 官方文档找到简单的示例&#xff0c;所以我认为先看看这个是个好主意。 连接客户端有两种方式&#xff0c;如下图。 至于两者的特点&#xff0c;TypedClient有类型&#xff0c;更容易编写&#xff0c;但文档较少。另外&…

以 RoCE+软件定义存储同时实现信创转型与架构升级

目前&#xff0c;不少企业数据中心使用 FC 交换机和集中式 SAN 存储&#xff08;以下简称“FC-SAN 架构”&#xff09;&#xff0c;支持核心业务系统、数据库、AI/ML 等高性能业务场景。而在开展 IT 基础架构信创转型时&#xff0c;很多用户受限于国外交换机&#xff1a;FC 交换…

往期精彩推荐

所有的内容都在这个博客中&#xff0c;此博客为推广导航博客&#xff0c;过后会删掉https://blog.csdn.net/weixin_41620184/article/details/135042416 往期精彩&#xff1a;快来学习吧~~~ 机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归PySpark大数据处…

SVM 保存和加载模型参数

在 Python 中&#xff0c;你可以使用 scikit-learn 库中的 joblib 或 pickle 模块来保存和加载 SVM 模型的参数。以下是一个简单的示例代码&#xff0c;演示了如何使用 joblib 模块保存和加载 SVM 模型的参数&#xff1a; 保存模型参数&#xff1a; from sklearn import svm …

向日葵远程工具安装Mysql的安装与配置

目录 一、向日葵远程工具安装 1.1 简介 1.2 下载地址 二、Mysql 5.7 安装与配置 2.1 简介 2.2 安装 2.3 初始化mysql服务端 2.4 启动mysql服务 2.5 登录mysql 2.6 修改密码 2.7 设置外部访问 三、思维导图 一、向日葵远程工具安装 1.1 简介 向日葵远程控制是一款用…

VS2017 搭建opencv工程

VS2017 搭建opencv工程 opencv在处理图像方面具有很强的能力&#xff0c;在使用opencv之前先需要造好轮子。 1、opencv 官网 &#xff0c;下载对应的资源文件包。 根据自身选择。下载包之后&#xff0c;解压。分为build和sources source目录下分别存放&#xff1a; modules: …

侯捷C++ 2.0 新特性

关键字 nullptr and std::nullptr_t auto 一致性初始化&#xff1a;Uniform Initialization 11之前&#xff0c;初始化方法包括&#xff1a;小括号、大括号、赋值号&#xff0c;这让人困惑。基于这个原因&#xff0c;给他来个统一&#xff0c;即&#xff0c;任何初始化都能够…

CSS3设置圆角化

可以使用border-radius属性来设置圆角化 有四个值 分别是&#xff1a;左上 右上 左下 右下 三个值 分别是&#xff1a;左上 右上左下 右下 两个值 分别是&#xff1a;左上右下 右上左下 给单独一角设置圆角化可以使用&#xff1a;border-xxx-xxx-radius 例&#xff1a…

pytorch 转 onnx

ONNX 是目前模型部署中最重要的中间表示之一&#xff0c;在把 PyTorch 模型转换成 ONNX 模型时&#xff0c;使用的 torch 接口是 torch.onnx.export 这里记录了 pytorch 模型转 onnx 时的原理和注意事项&#xff0c;还包括部分 PyTorch 与 ONNX 的算子对应关系。 1 torch.onnx…

React使用动态标签名称

最近在一项目里&#xff08;React antd&#xff09;遇到一个需求&#xff0c;某项基础信息里有个图标配置&#xff08;图标用的是antd的Icon组件&#xff09;&#xff0c;该项基础信息的图标信息修改后&#xff0c;存于后台数据库&#xff0c;后台数据库里存的是antd Icon组件…

用Redis实现实现全局唯一ID

全局唯一ID 如果使用数据库自增ID就存在一些问题&#xff1a; id的规律性太明显受表数据量的限制 全局ID生成器&#xff0c;是一种在分布式系统下用来生成全局唯一ID的工具&#xff0c;一般要满足下列特性&#xff1a; 唯一性高可用递增性安全性高性能 为了增加ID的安全性…

Django 后台与便签

1. 什么是后台管理 后台管理是网页管理员利用网页的后台程序管理和更新网站上网页的内容。各网站里网页内容更新就是通过网站管理员通过后台管理更新的。 2. 创建超级用户 1. python .\manage.py createsuperuser 2. 输入账号密码等信息 Username (leave blank to use syl…

在Android设备上设置和使用隧道代理HTTP

随着互联网的深入发展&#xff0c;网络信息的传递已经成为人们日常生活中不可或缺的一部分。对于我们中国人来说&#xff0c;由于某些特殊的原因&#xff0c;访问国外网站时常常会遇到限制。为了解决这个问题&#xff0c;使用代理服务器成为了许多人的选择。而在Android设备上设…