前言

前段时间在项目的开发过程中,遇到了需要从数据库中动态查询新的数据源信息并切换到该数据源做相应的查询操作,翻阅了网上很多资料都是简单的对多数据源的整合,并没有涉及到动态添加新数据源并切换的案例,本文根据自己在项目中遇到的问题,用简单案例的形式,对该部分内容进行讲解。

一、项目搭建

本块内容将最初的环境搭建,一步步讲解如何配置多数据源,动态数据源添加,以及一些工具类的编写。

1、pom文件配置

项目中需要用到的相关依赖。

        <!--web模块依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring核心aop模块依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!--德鲁伊数据源连接池依赖--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.8</version></dependency><!--mybatis依赖--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><!--mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.49</version></dependency><!--lombok模块依赖--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>

2、yml文件配置

server:port: 9090spring:datasource:druid:type: com.alibaba.druid.pool.DruidDataSourcemaster:url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave:url: jdbc:mysql://localhost:3306/db3?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driver#mybatis配置
mybatis:# 搜索指定包别名type-aliases-package: com.zy.fszl.domain# 配置mapper的扫描,找到所有的mapper.xml映射文件mapperLocations: classpath:mapper/*Mapper.xml# 加载全局的配置文件configLocation: classpath:mybatis/mybatis-config.xml

3、基于AbstractRoutingDataSource的数据源切换

在spring中有一个抽象类AbstractRoutingDataSource类,通过这个类可以实现动态数据源切换。 下面对AbstractRoutingDataSource类进行简单说明:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements 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();}

4、 DynamicDataSource类

了解了AbstractRoutingDataSource的大体流程之后我们需要编写DynamicDataSource类继承AbstractRoutingDataSource类并重写抽象方法determineCurrentLookupKey

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

5、常量类

/*** @Description:   动态数据源常量类* @Author zhangyu* @Date 2022/8/18 16:33**/
public class CommonConstant {/*** 默认数据源标识*/public static final String MASTER = "master";/*** 从数据源标识*/public static final String SLAVE = "slave";
}

6、DynamicDataSourceHolder

DynamicDataSourceHolder类主要是设置当前线程的数据源名称,移除数据源名称,以及获取当前数据源的名称,便于动态切换。

/*** 数据源切换处理** @author zhangyu*/
@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 ? CommonConstant.MASTER : key;}/*** 移除当前数据源*/public static void removeDynamicDataSourceKey(){log.info("移除数据源:{}",DYNAMIC_DATASOURCE_KEY.get());DYNAMIC_DATASOURCE_KEY.remove();}
}

7、DruidConfig

数据源信息配置类,读取数据源配置信息并注册成bean。

@Configuration
@MapperScan("com.zy.fszl.mapper")
public class DruidConfig {@Bean(name = CommonConstant.MASTER)@ConfigurationProperties("spring.datasource.druid.master")public DataSource masterDataSource(){DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean(name = CommonConstant.SLAVE)@ConfigurationProperties("spring.datasource.druid.slave")public DataSource slaveDataSource(){DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return dataSource;}@Bean@Primarypublic DynamicDataSource dynamicDataSource(){Map<Object, Object> dataSourceMap = new HashMap<>(2);dataSourceMap.put(CommonConstant.MASTER,masterDataSource());dataSourceMap.put(CommonConstant.SLAVE,slaveDataSource());//设置动态数据源DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setDefaultTargetDataSource(masterDataSource());dynamicDataSource.setTargetDataSources(dataSourceMap);//将数据源信息备份在defineTargetDataSources中dynamicDataSource.setDefineTargetDataSources(dataSourceMap);return dynamicDataSource;}
}

8、数据源工具类

用来测试连接并动态将测试成功的数据源信息添加到目标数据源集合中。

/*** @Description:   数据源工具类* @Author zhangyu* @Date 2022/8/18 17:20**/
@Slf4j
@Component
public class DataSourceUtil {@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.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();}
}

二、测试

通过案例对动态数据源的添加及切换进行验证,先从主库中查询汽车信息,然后通过DynamicDataSourceHolder.setDynamicDataSourceKey("切换数据源名称");        切换到从库查询数据源信息,并通过工具类测试数据源连接是否成功,数据源连接成功之后将数据源信息放到目标数据源集合中,并将targetDataSources的内容转化一下放到resolvedDataSources中,然后切换到从库查询所有用户信息,查询完成之后释放数据源连接。

@Slf4j
@RestController
public class TestController {@ResourceCommonMapper commonMapper;@ResourceDataSourceUtil dataSourceUtil;@GetMapping("/test")public Map<String, Object> dynamicDataSourceTest(){Map<String, Object> map = new HashMap<>();//在主库中查询汽车信息列表List<Car> carList = commonMapper.getCarInfo();map.put("car",carList);carList.forEach(car -> {log.info("汽车信息:{}",car);});//在从库中查询数据源信息DynamicDataSourceHolder.setDynamicDataSourceKey(CommonConstant.SLAVE);DataSourceInfo dataSourceInfo = commonMapper.getNewDataSourceInfo();map.put("dataSource",dataSourceInfo);log.info("数据源信息:{}",dataSourceInfo);//测试数据源连接DruidDataSource druidDataSource = dataSourceUtil.createDataSourceConnection(dataSourceInfo);if (Objects.nonNull(druidDataSource)){//将新的数据源连接添加到目标数据源map中dataSourceUtil.addDefineDynamicDataSource(druidDataSource,dataSourceInfo.getDatasourceKey());//设置当前线程数据源名称-----代码形式DynamicDataSourceHolder.setDynamicDataSourceKey(dataSourceInfo.getDatasourceKey());//在新的数据源中查询用户信息List<User> userList = commonMapper.getUserInfo();map.put("user",userList);userList.forEach(user -> {log.info("用户信息:{}",user);});//关闭数据源连接druidDataSource.close();}return map;}
}

可以看到map集合中的数据成功返回,说明数据源添加及切换成功。

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

从测试代码中可以看到,如果我们要想切换数据源需要在mapper调用之前调用DynamicDataSourceHolder.setDynamicDataSourceKey();方法,不够简洁优雅,所有我们可以通过注解的方式来动态进行数据源的切换。

1、创建注解类DataSource

/*** 自定义多数据源切换注解** 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准** @author ruoyi*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{/*** 切换数据源名称*/public String value() default CommonConstant.MASTER;
}

2、创建切面DataSourceAspect

@Aspect
@Component
public class DataSourceAspect {// 设置DataSource注解的切点表达式@Pointcut("@annotation(com.zy.fszl.annotation.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();}}/*** 先判断方法的注解,后判断类的注解,以方法的注解为准* @param joinPoint* @return*/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);}}}

3、进行数据源切换

只需要在对应mapper文件的方法上添加@DataSource注解即可

public interface CommonMapper {List<Car> getCarInfo();@DataSource(value = CommonConstant.SLAVE)DataSourceInfo getNewDataSourceInfo();//设置当前数据源名称------注解形式@DataSource(value = "demo1")List<User> getUserInfo();
}

SpringBoot整合多数据源,动态添加新数据源并切换(保姆级教程)相关推荐

  1. springboot整合quartz实现动态添加、修改、删除、停止job,以及优化quartz工具类,支持自动停止逻辑

    原文链接:http://pengfeiguo.com/article/16 什么是Quartz? 一个定时任务调度框架,简单易用,功能强大可以使实现定时任务的. 优点: 支持集群下定时任务处理 支持任 ...

  2. SM2258XT添加Flash支持列表(保姆级教程,附个人经验+SM2258XT量产开卡工具)

    写在前面:最近发现关于2258XT添加FlashDB文件中数据后仍然不能识别Flash情况,以及FlashDB当中有对应的Flash信息,却在开卡时无法选择对应的Flash片数据.针对以上两种情况,个 ...

  3. DropDownList动态添加新项并位于第一项 ASP.NET控件应用

    今天做一个查询功能时遇到了一个问题,是这样的: 我要用DropDownList来绑定数据库里查询出来的类型数据,如下图:想在这两个类型之上再增加一个"全部留言"的项,因为查询条件应 ...

  4. 转——C# DataGridView控件 动态添加新行

    DataGridView控件在实际应用中非常实用,特别需要表格显示数据时.可以静态绑定数据源,这样就自动为DataGridView控件添加相应的行.假如需要动态为DataGridView控件添加新行, ...

  5. DataGridView动态添加新行的两种方法

    简单介绍如何为DataGridView控件动态添加新行的两种方 法: 方法一: int index=this.dataGridView1.Rows.Add(); this.dataGridView1. ...

  6. 快速上手Springboot项目(登录注册保姆级教程)

    本文章对SpringBoot开发后端项目结构做了简单介绍,并示范了使用SpringBoot+MySQL实现登录的后端功能,与本博客的另一篇文章 Vue 实现登录注册功能(前后端分离完整案例) | Ma ...

  7. 保姆级教程——将springboot项目部署到阿里云服务器(小白包会)

    保姆级教程--将springboot项目部署到阿里云服务器(小白包会) 前言: 昨天本想着看论文,结果发现找的论文和课题不一致.那干点什么好呢?那就把我的毕业设计(一个springboot项目)部署到 ...

  8. SpringBoot 配置 generator代码生成+knife4j接口文档(2种模板设置、逻辑删除、字段填充 含代码粘贴可用)保姆级教程(注意事项+建表SQL+代码生成类封装+测试类)

    保姆级教程,逻辑删除及字段自动填充设置,特别要说明的是本次用的是MySQL数据库,如果使用Oracle数据库是,数据库配置需要改变,数据库表一定要大写,否则无法生成代码. 数据库表 CREATE TA ...

  9. Linux挂载新硬盘【保姆级教程】

    目录 写在前面 Get服务器的IP地址.账密 保姆级挂载指南 设置开机自动挂载硬盘 写在前面 [注意]你的电脑连接学校的无线网或者通过学校的网线端口上网,否则你是连不上学校服务器,也不能运行代码!!! ...

最新文章

  1. ecs安装tomcat和mysql_centos(Linux)系统阿里云ECS搭建 jdk,tomcat和MySQL环境,并部署web程序...
  2. 04使用模板显示内容
  3. mysql数据库语句q_mysql数据库命令大全,mysql基本命令大全
  4. Python 工匠:在边界处思考
  5. POJ - 2516 Minimum Cost(最小费用最大流)
  6. C#读书笔记:线程,任务和同步
  7. Oracle基于布尔的盲注总结
  8. 前端月薪过万需要哪些技术_Web前端月薪过万必修的几项技能,你会吗?
  9. date时区 es logstash_elastic date时区问题解决办法
  10. Silverlight中使用MVVM(9)-绑定Enum类型数据
  11. 如何用Baas快速在腾讯云上开发小程序-系列3 :实现腾讯云COS API调用
  12. Ubuntu 16.04 安裝chrome
  13. c++怎么可以在二进制文件中读取带string的数据_文件处理 | csv文件读写
  14. shape用法的详细举例
  15. 杀毒软件工作原理 及 现在主要杀毒技术
  16. win10关闭快速启动_关闭Win10中这七个不必要的服务,解决电脑卡顿问题。
  17. 电脑遇到蓝屏的时候怎么解决
  18. Premiere pro在图片中插入视频元素
  19. nginx+rtmp+OBS搭建音视频直播服务
  20. 版本发布 | IvorySQL Release - 2.2

热门文章

  1. 态度决定命运,将自己的梦想抛向天空,你就能飞翔
  2. 微信支付:chooseWXPay:fail, the permission value is offline verifying
  3. HTML5从入门到精通,零基础学员必看
  4. java双向链表详解
  5. java jce.jar_Java JCE无法在jarsplice创建的jar中验证提供者BC
  6. 手机注册(发送短信验证码)
  7. MTK Camera 开机启动流程
  8. ASE28N50-ASEMI高压MOS管ASE28N50
  9. signed main()
  10. welcome-file 不能配置action的解决方法