文章目录
动态数据源DynamicDatasource
简介
本文介绍baomidou开源的多数据源扩展插件DynamicDatasource,并用来实现动态数据源及以下功能
介绍功能包括:
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
DynamicDatasource项目地址、文档地址
本文Demo完整源码:
Github源代码地址:https://github.com/xunfeng224/Springboot/tree/main/springboot-DynamicDatasource
Gitee源代码地址:https://gitee.com/xfeng520/Springboot/tree/main/springboot-DynamicDatasource
[!CAUTION]
运行源码需注意,由于加入了手动实现的动态数据源,会导致Bean冲突,若想运行DynamicDataSource,将手动代码dynamic包直接删除,若想运行手动实现的动态数据源,将
LoadDataSourceRunner
类全部注释掉,避免找不到Bean报错。手动动态数据源相关代码全部位于dynamic包下。
原因:自定义数据源管理类
DynamicDataSource
继承了Spring的
AbstractRoutingDataSource
类,而在DynamicDataSource源码中,
DynamicRoutingDataSource
类同样继承了
AbstractRoutingDataSource
,本来想通过自定义Bean注入名称来解决Bean冲突,发现不可行。在
DynamicDataSourceAutoConfiguration
中注册
DynamicRoutingDataSource
的bean时,加入了
@ConditionalOnMissingBean
注解,这将导致有自定义实现类bean注入Spring容器时,
DynamicRoutingDataSource
无法注入Spring容器,从而启动报错。
@ConditionalOnMissingBean作用:判断当前需要注入Spring容器中的bean的实现类是否已经含有,有的话不注入,没有就注入
@Bean@ConditionalOnMissingBeanpublicDataSourcedataSource(List<DynamicDataSourceProvider> providers){DynamicRoutingDataSource dataSource =newDynamicRoutingDataSource(providers); dataSource.setPrimary(this.properties.getPrimary()); dataSource.setStrict(this.properties.getStrict()); dataSource.setStrategy(this.properties.getStrategy()); dataSource.setP6spy(this.properties.getP6spy()); dataSource.setSeata(this.properties.getSeata()); dataSource.setGraceDestroy(this.properties.getGraceDestroy());return dataSource;}
源码分析
ThreadLocal和AbstractRoutingDataSource
ThreadLocal
:全称:
thread local variable
。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本,确保每个线程在某一时间访问到的不是同一个对象,这样做到了隔离性,增加了内存,但大大减少了线程同步时的性能消耗,减少了线程并发控制的复杂程度。
- ThreadLocal作用:在一个线程中共享,不同线程间隔离
- ThreadLocal原理:ThreadLocal存入值时,会获取当前线程实例作为key,存入当前线程对象中的Map中。
AbstractRoutingDataSource
:根据用户定义的规则选择当前的数据源,
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
DynamicRoutingDataSource
该类继承上述提到的
AbstractRoutingDataSource
抽象类,实现
determineDataSource()
方法,如上文所述,该方法决定了当前数据库操作所使用的数据源
publicclassDynamicRoutingDataSourceextendsAbstractRoutingDataSourceimplementsInitializingBean,DisposableBean{//...省略... /**
* 通过各种方式加载的数据源将存储在该Map中,后续动态切换也是从这里获取
*/privatefinalMap<String,DataSource> dataSourceMap =newConcurrentHashMap<>();/**
* 分组数据库
*/privatefinalMap<String,GroupDataSource> groupDataSources =newConcurrentHashMap<>();/**
获取主数据源
*/@OverrideprotectedStringgetPrimary(){return primary;}@OverridepublicDataSourcedetermineDataSource(){// 数据源key/数据源名称从DynamicDataSourceContextHolder.peek()中获取String dsKey =DynamicDataSourceContextHolder.peek();returngetDataSource(dsKey);}/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/publicDataSourcegetDataSource(String ds){if(DsStrUtils.isEmpty(ds)){// 这里数据源名称为空,调用方法获取主数据源returndeterminePrimaryDataSource();}elseif(!groupDataSources.isEmpty()&& groupDataSources.containsKey(ds)){
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);return groupDataSources.get(ds).determineDataSource();}elseif(dataSourceMap.containsKey(ds)){
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);return dataSourceMap.get(ds);}if(strict){thrownewCannotFindDataSourceException("dynamic-datasource could not find a datasource named "+ ds);}returndeterminePrimaryDataSource();}/**
* 添加数据源
*
* @param ds 数据源名称
* @param dataSource 数据源
*/publicsynchronizedvoidaddDataSource(String ds,DataSource dataSource){DataSource oldDataSource = dataSourceMap.put(ds, dataSource);// 新数据源添加到分组this.addGroupDataSource(ds, dataSource);// 关闭老的数据源if(oldDataSource !=null){closeDataSource(ds, oldDataSource, graceDestroy);}
log.info("dynamic-datasource - add a datasource named [{}] success", ds);}//...省略... }
DynamicDataSourceContextHolder
类 ,源码自带注解也挺详细的了,不做多解释
importorg.springframework.core.NamedThreadLocal;importjava.util.ArrayDeque;importjava.util.Deque;/**
* 核心基于ThreadLocal的切换数据源工具类
*
* @author TaoYu Kanyuxia
* @since 1.0.0
*/publicfinalclassDynamicDataSourceContextHolder{/**
* 为什么要用链表存储(准确的是栈)
* <pre>
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
* </pre>
*/privatestaticfinalThreadLocal<Deque<String>>LOOKUP_KEY_HOLDER=newNamedThreadLocal<Deque<String>>("dynamic-datasource"){@OverrideprotectedDeque<String>initialValue(){returnnewArrayDeque<>();}};privateDynamicDataSourceContextHolder(){}/**
* 获得当前线程数据源
*
* @return 数据源名称
*/publicstaticStringpeek(){returnLOOKUP_KEY_HOLDER.get().peek();}/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
* @return 数据源名称
*/publicstaticStringpush(String ds){String dataSourceStr =DsStrUtils.isEmpty(ds)?"": ds;LOOKUP_KEY_HOLDER.get().push(dataSourceStr);return dataSourceStr;}/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
* </p>
*/publicstaticvoidpoll(){Deque<String> deque =LOOKUP_KEY_HOLDER.get();
deque.poll();if(deque.isEmpty()){LOOKUP_KEY_HOLDER.remove();}}/**
* 强制清空本地线程
* <p>
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
* </p>
*/publicstaticvoidclear(){LOOKUP_KEY_HOLDER.remove();}}
DynamicDatasource快速开始
其实也没啥好写的,DynamicDatasource功能很丰富,但本文章只涉及到简单的操作。流程为新建Springboot项目,引入Maven依赖,配置yml中的master数据源,使用mybatis-plus快速实现查询主数据源sys_user表数据,通过defaultDataSourceCreator.createDataSource(dataSourceProperty)创建数据源,通过dynamicRoutingDataSource.addDataSource(ds.getId().toString(), dataSource);添加数据源,通过DynamicDataSourceContextHolder.push(dsId.toString());切换数据源或通过注解@DS(“master”)切换
项目结构
Maven依赖
引入DynamicDatasource依赖
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.3.1</version></dependency>
引入其他依赖,为本案例项目所需依赖,非实现动态数据源所必须
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version></dependency><dependency><groupId>com.baomidou</groupId><!--下面坐标根据自己使用的SpringBoot版本二选一--><!--SpringBoot2使用此版本--><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>4.3.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.23</version></dependency><dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId><version>42.2.18</version></dependency>
application.yml配置文件
其中spring.datasource.dynamic.datasource即为DynamicDatasource插件所需的配置文件,master为主数据源,主数据源是必须的,但不是一定得从配置文件设置,这里不过多展开,后文介绍逻辑会将明白。
# 应用服务 WEB 访问端口server:port:8080spring:datasource:dynamic:primary: master
datasource:# 主数据源master:driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${DB_HOST:127.0.0.1}:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=trueusername: ${DB_USER:root}password: ${DB_PASSWORD:root}type: com.alibaba.druid.pool.DruidDataSource
druid:# 等待时间毫秒max-wait:3000# 重试次数connection-error-retry-attempts:3# 失败后break退出循环,若为false,当getConnection失败时会无限重试break-after-acquire-failure:true
sql脚本
sys_user表用于测试获取数据,data_source表存放动态数据源
SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS =0;-- ------------------------------ Table structure for sys_user-- ----------------------------DROPTABLEIFEXISTS`sys_user`;CREATETABLE`sys_user`(`user_id`bigintNOTNULLCOMMENT'用户ID',`username`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'用户名',`password`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'密码',`salt`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'盐值',`phone`varchar(20)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'电话号码',`avatar`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'头像',`nickname`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'昵称',`name`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'姓名',`email`varchar(128)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'邮箱地址',`dept_id`bigintNULLDEFAULTNULLCOMMENT'所属部门ID',`create_by`varchar(64)CHARACTERSET utf8mb3 COLLATE utf8mb3_general_ci NOTNULLDEFAULT' 'COMMENT'创建人',`update_by`varchar(64)CHARACTERSET utf8mb3 COLLATE utf8mb3_general_ci NOTNULLDEFAULT' 'COMMENT'修改人',`create_time`datetimeNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`update_time`datetimeNULLDEFAULTNULLONUPDATECURRENT_TIMESTAMPCOMMENT'修改时间',`lock_flag`char(1)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULT'0'COMMENT'锁定标记,0未锁定,9已锁定',`del_flag`char(1)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULT'0'COMMENT'删除标记,0未删除,1已删除',`wx_openid`varchar(32)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'微信登录openId',`mini_openid`varchar(32)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'小程序openId',`qq_openid`varchar(32)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'QQ openId',`gitee_login`varchar(100)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'码云标识',`osc_id`varchar(100)CHARACTERSET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULLCOMMENT'开源中国标识',PRIMARYKEY(`user_id`)USINGBTREE,INDEX`user_wx_openid`(`wx_openid`)USINGBTREE,INDEX`user_qq_openid`(`qq_openid`)USINGBTREE,INDEX`user_idx1_username`(`username`)USINGBTREE)ENGINE=InnoDBCHARACTERSET= utf8mb4 COLLATE= utf8mb4_general_ci COMMENT='用户表' ROW_FORMAT = Dynamic;-- ------------------------------ Records of sys_user-- ----------------------------INSERTINTO`sys_user`VALUES(1,'admin','$2a$10$8KIKR17eIM4VZFycIYRycOoW/4q0pFuFg4y/16lMm9aPQ1n4.vXx6','','17034642999','/admin/sys-file/local/7fba53c7f1ff449aa22d60660498cf61.jpg','管理员','管理员','[email protected]',4,' ','admin','2018-04-20 07:15:18','2024-05-09 18:12:49','0','0',NULL,'oBxPy5E-v82xWGsfzZVzkD3wEX64',NULL,'log4j',NULL);-- ------------------------------ Table structure for data_source-- ----------------------------DROPTABLEIFEXISTS`data_source`;CREATETABLE`data_source`(`id`bigintNOTNULLAUTO_INCREMENTCOMMENT'数据源主键id',`type`char(1)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'数据源类型',`driver_class_name`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'数据库驱动',`url`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'数据库地址',`name`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NOTNULLCOMMENT'连接名称',`host`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NOTNULLCOMMENT'主机名或IP地址',`port`intNULLDEFAULTNULLCOMMENT'端口号默认3306',`username`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'用户名',`password`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'密码',`data_base`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'数据库名',`param`varchar(255)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'参数',`state`char(1)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'数据源状态:0连接失败,1连接成功',`del_flag`char(1)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULT'0'COMMENT'删除标志(0代表存在 1代表删除)',`create_by`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULT''COMMENT'创建者',`create_time`datetimeNULLDEFAULTNULLCOMMENT'创建时间',`update_by`varchar(64)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULT''COMMENT'更新者',`update_time`datetimeNULLDEFAULTNULLCOMMENT'更新时间',`remark`varchar(5000)CHARACTERSET utf8mb4 COLLATE utf8mb4_bin NULLDEFAULTNULLCOMMENT'备注',PRIMARYKEY(`id`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=3CHARACTERSET= utf8mb4 COLLATE= utf8mb4_bin COMMENT='数据源管理表' ROW_FORMAT = DYNAMIC;-- ------------------------------ Records of data_source-- ----------------------------INSERTINTO`data_source`VALUES(1,NULL,'com.mysql.cj.jdbc.Driver','jdbc:mysql://192.168.252.15:3306/AIGC?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true&useInformationSchema=true','银行业务数据库','192.168.252.15',3306,'hithium','Hithium@Dev2024',NULL,NULL,NULL,'0','',NULL,'',NULL,NULL);INSERTINTO`data_source`VALUES(2,NULL,'org.postgresql.Driver','jdbc:postgresql://127.0.0.1:5432/test_db','openGauss数据库','127.0.0.1',5432,'gaussdb','Enmo@123',NULL,NULL,NULL,'0','',NULL,'',NULL,NULL);SET FOREIGN_KEY_CHECKS =1;
动态加载数据源
这里从主数据库data_source表中获取数据源信息,并创建DruidDatasource,并通过dynamicRoutingDataSource.addDataSource()方法将Datasource进行统一管理
LoadDataSourceRunner.java
packagecom.xunfeng.example.init;importcom.baomidou.dynamic.datasource.DynamicRoutingDataSource;importcom.baomidou.dynamic.datasource.creator.DataSourceProperty;importcom.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;importcom.baomidou.mybatisplus.core.toolkit.CollectionUtils;importcom.xunfeng.example.domain.entity.DataSourceEntity;importcom.xunfeng.example.mapper.DataSourceMapper;importorg.springframework.beans.BeanUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.stereotype.Component;importjavax.sql.DataSource;importjava.sql.Connection;importjava.sql.SQLException;importjava.util.List;/**
* @author
* @date 2024/6/17 14:31
*/@ComponentpublicclassLoadDataSourceRunnerimplementsCommandLineRunner{@AutowiredprivateDataSourceMapper dataSourceMapper;@AutowiredprivateDefaultDataSourceCreator defaultDataSourceCreator;@AutowiredprivateDynamicRoutingDataSource dynamicRoutingDataSource;@Overridepublicvoidrun(String... args){List<DataSourceEntity> dataSourceEntities = dataSourceMapper.selectList(null);if(CollectionUtils.isNotEmpty(dataSourceEntities)){for(DataSourceEntity ds : dataSourceEntities){DataSourceProperty dataSourceProperty =newDataSourceProperty();BeanUtils.copyProperties(ds, dataSourceProperty);DataSource dataSource = defaultDataSourceCreator.createDataSource(dataSourceProperty);Connection connection =null;try{
connection = dataSource.getConnection();// 本质上是个Map,采用key-value形式存储数据源,后续获取数据源需要key
dynamicRoutingDataSource.addDataSource(ds.getId().toString(), dataSource);}catch(SQLException e){System.out.println(e.getMessage());}finally{try{if(connection !=null){
connection.close();}}catch(SQLException e){thrownewRuntimeException(e);}}}}}}
动态切换数据源
1.代码中切换:DynamicDataSourceContextHolder.push(【数据源key】);
2.注解形式切换@DS(“master”)
packagecom.xunfeng.example.controller;importcom.baomidou.dynamic.datasource.annotation.DS;importcom.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;importcom.xunfeng.example.domain.entity.SysUser;importcom.xunfeng.example.mapper.SysUserMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;/**
* @author
* @date 2024/8/5 10:27
*/@RestController@RequestMapping("user")publicclassUserController{@AutowiredprivateSysUserMapper sysUserMapper;@GetMapping("/{dsId}/{userId}")publicStringgetUser(@PathVariable("dsId")String dsId,@PathVariable("userId")Long userId){DynamicDataSourceContextHolder.push(dsId.toString());SysUser sysUser = sysUserMapper.selectById(userId);return sysUser.toString();}@GetMapping("/{userId}")@DS("master")publicStringgetUserDS(@PathVariable("dsId")String dsId,@PathVariable("userId")Long userId){DynamicDataSourceContextHolder.push(dsId.toString());SysUser sysUser = sysUserMapper.selectById(userId);return sysUser.toString();}}
手动实现
模仿DynamicDatasource手动实现简单动态数据源,如果不需要dynamicDatasource那么复杂的功能,可以考虑手动实现。
核心类
DataSourceContextHolder
packagecom.xunfeng.example.dynamic;/**
* @author
* @date 2024/6/17 14:20
*/publicclassDataSourceContextHolder{/**
* 此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
*/privatestaticfinalThreadLocal<String>DATASOURCE_HOLDER=newThreadLocal<>();/**
* 设置数据源
*
* @param dataSourceName 数据源名称
*/publicstaticvoidsetDataSource(String dataSourceName){DATASOURCE_HOLDER.set(dataSourceName);}/**
* 获取当前线程的数据源
*
* @return 数据源名称
*/publicstaticStringgetDataSource(){returnDATASOURCE_HOLDER.get();}/**
* 删除当前数据源
*/publicstaticvoidremoveDataSource(){DATASOURCE_HOLDER.remove();}}
核心类
DynamicDataSource
packagecom.xunfeng.example.dynamic;importcom.alibaba.druid.pool.DruidDataSource;importcom.baomidou.mybatisplus.core.toolkit.CollectionUtils;importcom.xunfeng.example.domain.entity.DataSourceEntity;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.BeanUtils;importorg.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;importjavax.sql.DataSource;importjava.sql.DriverManager;importjava.sql.SQLException;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;/**
* @author
* @date 2024/6/17 14:19
*/@Slf4jpublicclassDynamicDataSourceextendsAbstractRoutingDataSource{privatefinalstaticMap<Object,Object> targetDataSourceMap =newHashMap<>();publicDynamicDataSource(DataSource defaultDataSource,Map<Object,Object> targetDataSources){super.setDefaultTargetDataSource(defaultDataSource);super.setTargetDataSources(targetDataSources);
targetDataSourceMap.put("master", defaultDataSource);// targetDataSourceMap = targetDataSources;}@OverrideprotectedObjectdetermineCurrentLookupKey(){String dataSource =DataSourceContextHolder.getDataSource();DataSourceContextHolder.removeDataSource();return dataSource;}/**
* 添加数据源信息
*
* @param dataSources 数据源实体集合
* @return 返回添加结果
*/publicBooleancreateDataSource(List<DataSourceEntity> dataSources){if(CollectionUtils.isNotEmpty(dataSources)){for(DataSourceEntity ds : dataSources){try{//校验数据库是否可以连接Class.forName(ds.getDriverClassName());DriverManager.getConnection(ds.getUrl(), ds.getUsername(), ds.getPassword());//定义数据源DruidDataSource dataSource =newDruidDataSource();BeanUtils.copyProperties(ds, dataSource);//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用
dataSource.setTestOnBorrow(true);//建议配置为true,不影响性能,并且保证安全性。//申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
dataSource.setTestWhileIdle(true);//用来检测连接是否有效的sql,要求是一个查询语句。
dataSource.setValidationQuery("select 1 ");
dataSource.init();this.targetDataSourceMap.put(ds.getId(), dataSource);}catch(ClassNotFoundException|SQLException e){
log.error("---数据源初始化错误---:{}", e.getMessage());}}super.setTargetDataSources(targetDataSourceMap);// 将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();returnBoolean.TRUE;}returnBoolean.FALSE;}/**
* 校验数据源是否存在
*
* @param key 数据源保存的key
* @return 返回结果,true:存在,false:不存在
*/publicstaticbooleanexistsDataSource(Long key){returnObjects.nonNull(targetDataSourceMap.get(key));}publicstaticMap<Object,Object>getTargetDataSourceMap(){return targetDataSourceMap;}}
核心类
DynamicDataSourceConfig
这里主要功能为注册主数据源,也可以在这里注册更多的其他数据源
packagecom.xunfeng.example.dynamic;importcom.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Primary;importjavax.sql.DataSource;importjava.util.HashMap;importjava.util.Map;/**
* @author
* @date 2024/6/17 14:54
*/@ConfigurationpublicclassDynamicDataSourceConfig{@Bean@ConfigurationProperties("spring.datasource.dynamic.datasource.master")publicDataSourcemasterDataSource(){returnDruidDataSourceBuilder.create().build();}@Bean(name ="anotherDynamicDataSource")@PrimarypublicDynamicDataSourcedataSource(){Map<Object,Object> dataSourceMap =newHashMap<>();DataSource defaultDataSource =masterDataSource();
dataSourceMap.put("master",defaultDataSource);returnnewDynamicDataSource(defaultDataSource,dataSourceMap);}}
服务启动加载数据源类
AnotherLoadDataSourceRunner
packagecom.xunfeng.example.dynamic.init;importcom.baomidou.mybatisplus.core.toolkit.CollectionUtils;importcom.xunfeng.example.domain.entity.DataSourceEntity;importcom.xunfeng.example.dynamic.DynamicDataSource;importcom.xunfeng.example.mapper.DataSourceMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.CommandLineRunner;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;importjava.util.List;/**
* @author
* @date 2024/6/17 14:31
*/@ComponentpublicclassAnotherLoadDataSourceRunnerimplementsCommandLineRunner{@AutowiredprivateDataSourceMapper dataSourceMapper;@Resource(name ="anotherDynamicDataSource")privateDynamicDataSource anotherDynamicDataSource;@Overridepublicvoidrun(String... args)throwsException{List<DataSourceEntity> list = dataSourceMapper.selectList(null);if(CollectionUtils.isNotEmpty(list)){
anotherDynamicDataSource.createDataSource(list);}}}
自定义数据源切换注解
DataSource
packagecom.xunfeng.example.dynamic.annotation;importjava.lang.annotation.*;/**
* @author
* @date 2024/6/17 15:17
*/@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceDataSource{Stringvalue()default"master";}
切面
DSAspect
packagecom.xunfeng.example.dynamic.aspect;importcom.baomidou.mybatisplus.core.toolkit.StringUtils;importcom.xunfeng.example.domain.entity.DataSourceEntity;importcom.xunfeng.example.dynamic.DataSourceContextHolder;importcom.xunfeng.example.dynamic.annotation.DataSource;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.stereotype.Component;importjava.lang.reflect.Method;importjava.util.Objects;/**
* @author
* @date 2024/6/17 15:18
*/@Aspect@ComponentpublicclassDSAspect{@Pointcut("@annotation(com.xunfeng.example.dynamic.annotation.DataSource)")publicvoiddatasourcePoint(){}@Around("datasourcePoint()")publicObjectdatasourceAround(ProceedingJoinPoint point)throwsThrowable{MethodSignature signature =(MethodSignature) point.getSignature();Method method = signature.getMethod();DataSource dataSource = method.getAnnotation(DataSource.class);if(Objects.nonNull(dataSource)){// 数据源keyString key =null;// 1.从入参中获取数据源key,并切换Object[] args = point.getArgs();for(Object arg : args){// 自定义入参标准,这里简单用id作为keyif(arg instanceofDataSourceEntity){DataSourceEntity req =(DataSourceEntity) arg;
key = req.getId().toString();}}// 2.获取注解中的value为数据源keyif(StringUtils.isEmpty(key)){
key = dataSource.value();}// 实时切换默认数据源DataSourceContextHolder.setDataSource(key);}try{return point.proceed();}finally{DataSourceContextHolder.removeDataSource();}}}
版权归原作者 寻风224 所有, 如有侵权,请联系我们删除。