文章目录
- 动态数据源DynamicDatasource
-
- 简介
- 源码分析
-
- ThreadLocal和AbstractRoutingDataSource
- DynamicRoutingDataSource
- DynamicDatasource快速开始
-
- 项目结构
- Maven依赖
- application.yml配置文件
- sql脚本
- 动态加载数据源
- 动态切换数据源
- 手动实现
-
- 核心类`DataSourceContextHolder`
- 核心类`DynamicDataSource`
- 核心类`DynamicDataSourceConfig`
- 服务启动加载数据源类`AnotherLoadDataSourceRunner`
- 自定义数据源切换注解`DataSource`
- 切面`DSAspect`
动态数据源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@ConditionalOnMissingBeanpublic DataSource dataSource(List<DynamicDataSourceProvider> providers) {DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(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()
方法,如上文所述,该方法决定了当前数据库操作所使用的数据源
public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {//...省略... /*** 通过各种方式加载的数据源将存储在该Map中,后续动态切换也是从这里获取*/private final Map<String, DataSource> dataSourceMap = new ConcurrentHashMap<>();/*** 分组数据库*/private final Map<String, GroupDataSource> groupDataSources = new ConcurrentHashMap<>();/**获取主数据源*/@Overrideprotected String getPrimary() {return primary;}@Overridepublic DataSource determineDataSource() {// 数据源key/数据源名称从DynamicDataSourceContextHolder.peek()中获取String dsKey = DynamicDataSourceContextHolder.peek();return getDataSource(dsKey);}/*** 获取数据源** @param ds 数据源名称* @return 数据源*/public DataSource getDataSource(String ds) {if (DsStrUtils.isEmpty(ds)) {// 这里数据源名称为空,调用方法获取主数据源return determinePrimaryDataSource();} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {log.debug("dynamic-datasource switch to the datasource named [{}]", ds);return groupDataSources.get(ds).determineDataSource();} else if (dataSourceMap.containsKey(ds)) {log.debug("dynamic-datasource switch to the datasource named [{}]", ds);return dataSourceMap.get(ds);}if (strict) {throw new CannotFindDataSourceException("dynamic-datasource could not find a datasource named " + ds);}return determinePrimaryDataSource();}/*** 添加数据源** @param ds 数据源名称* @param dataSource 数据源*/public synchronized void addDataSource(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
类 ,源码自带注解也挺详细的了,不做多解释
import org.springframework.core.NamedThreadLocal;import java.util.ArrayDeque;
import java.util.Deque;/*** 核心基于ThreadLocal的切换数据源工具类** @author TaoYu Kanyuxia* @since 1.0.0*/
public final class DynamicDataSourceContextHolder {/*** 为什么要用链表存储(准确的是栈)* <pre>* 为了支持嵌套切换,如ABC三个service都是不同的数据源* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。* 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。* </pre>*/private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {@Overrideprotected Deque<String> initialValue() {return new ArrayDeque<>();}};private DynamicDataSourceContextHolder() {}/*** 获得当前线程数据源** @return 数据源名称*/public static String peek() {return LOOKUP_KEY_HOLDER.get().peek();}/*** 设置当前线程数据源* <p>* 如非必要不要手动调用,调用后确保最终清除* </p>** @param ds 数据源名称* @return 数据源名称*/public static String push(String ds) {String dataSourceStr = DsStrUtils.isEmpty(ds) ? "" : ds;LOOKUP_KEY_HOLDER.get().push(dataSourceStr);return dataSourceStr;}/*** 清空当前线程数据源* <p>* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称* </p>*/public static void poll() {Deque<String> deque = LOOKUP_KEY_HOLDER.get();deque.poll();if (deque.isEmpty()) {LOOKUP_KEY_HOLDER.remove();}}/*** 强制清空本地线程* <p>* 防止内存泄漏,如手动调用了push可调用此方法确保清除* </p>*/public static void clear() {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: 8080
spring:datasource:dynamic:primary: masterdatasource:# 主数据源master:driver-class-name: com.mysql.cj.jdbc.Driverurl: 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.DruidDataSourcedruid:# 等待时间毫秒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
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (`user_id` bigint NOT NULL COMMENT '用户ID',`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '盐值',`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话号码',`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',`nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱地址',`dept_id` bigint NULL DEFAULT NULL COMMENT '所属部门ID',`create_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT ' ' COMMENT '创建人',`update_by` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT ' ' COMMENT '修改人',`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',`lock_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '锁定标记,0未锁定,9已锁定',`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',`wx_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信登录openId',`mini_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序openId',`qq_openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'QQ openId',`gitee_login` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '码云标识',`osc_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '开源中国标识',PRIMARY KEY (`user_id`) USING BTREE,INDEX `user_wx_openid`(`wx_openid`) USING BTREE,INDEX `user_qq_openid`(`qq_openid`) USING BTREE,INDEX `user_idx1_username`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$8KIKR17eIM4VZFycIYRycOoW/4q0pFuFg4y/16lMm9aPQ1n4.vXx6', '', '17034642999', '/admin/sys-file/local/7fba53c7f1ff449aa22d60660498cf61.jpg', '管理员', '管理员', 'pig4cloud@qq.com', 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
-- ----------------------------
DROP TABLE IF EXISTS `data_source`;
CREATE TABLE `data_source` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '数据源主键id',`type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '数据源类型',`driver_class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '数据库驱动',`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '数据库地址',`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '连接名称',`host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '主机名或IP地址',`port` int NULL DEFAULT NULL COMMENT '端口号默认3306',`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户名',`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '密码',`data_base` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '数据库名',`param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '参数',`state` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '数据源状态:0连接失败,1连接成功',`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '' COMMENT '创建者',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT '' COMMENT '更新者',`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',`remark` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '数据源管理表' ROW_FORMAT = DYNAMIC;-- ----------------------------
-- Records of data_source
-- ----------------------------
INSERT INTO `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);
INSERT INTO `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
package com.xunfeng.example.init;import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.xunfeng.example.domain.entity.DataSourceEntity;
import com.xunfeng.example.mapper.DataSourceMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;/*** @author* @date 2024/6/17 14:31*/
@Component
public class LoadDataSourceRunner implements CommandLineRunner {@Autowiredprivate DataSourceMapper dataSourceMapper;@Autowiredprivate DefaultDataSourceCreator defaultDataSourceCreator;@Autowiredprivate DynamicRoutingDataSource dynamicRoutingDataSource;@Overridepublic void run(String... args) {List<DataSourceEntity> dataSourceEntities = dataSourceMapper.selectList(null);if (CollectionUtils.isNotEmpty(dataSourceEntities)) {for (DataSourceEntity ds : dataSourceEntities) {DataSourceProperty dataSourceProperty = new DataSourceProperty();BeanUtils.copyProperties(ds, dataSourceProperty);DataSource dataSource = defaultDataSourceCreator.createDataSource(dataSourceProperty);Connection connection = null;try {connection = dataSource.getConnection();// 本质上是个Map,采用key-value形式存储数据源,后续获取数据源需要keydynamicRoutingDataSource.addDataSource(ds.getId().toString(), dataSource);} catch (SQLException e) {System.out.println(e.getMessage());} finally {try {if (connection != null) {connection.close();}} catch (SQLException e) {throw new RuntimeException(e);}}}}}
}
动态切换数据源
1.代码中切换:DynamicDataSourceContextHolder.push(【数据源key】);
2.注解形式切换@DS(“master”)
package com.xunfeng.example.controller;import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.xunfeng.example.domain.entity.SysUser;
import com.xunfeng.example.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author* @date 2024/8/5 10:27*/
@RestController
@RequestMapping("user")
public class UserController {@Autowiredprivate SysUserMapper sysUserMapper;@GetMapping("/{dsId}/{userId}")public String getUser(@PathVariable("dsId") String dsId, @PathVariable("userId") Long userId) {DynamicDataSourceContextHolder.push(dsId.toString());SysUser sysUser = sysUserMapper.selectById(userId);return sysUser.toString();}@GetMapping("/{userId}")@DS("master")public String getUserDS(@PathVariable("dsId") String dsId, @PathVariable("userId") Long userId) {DynamicDataSourceContextHolder.push(dsId.toString());SysUser sysUser = sysUserMapper.selectById(userId);return sysUser.toString();}
}
手动实现
模仿DynamicDatasource手动实现简单动态数据源,如果不需要dynamicDatasource那么复杂的功能,可以考虑手动实现。
核心类DataSourceContextHolder
package com.xunfeng.example.dynamic;/*** @author * @date 2024/6/17 14:20*/
public class DataSourceContextHolder {/*** 此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。*/private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();/*** 设置数据源** @param dataSourceName 数据源名称*/public static void setDataSource(String dataSourceName) {DATASOURCE_HOLDER.set(dataSourceName);}/*** 获取当前线程的数据源** @return 数据源名称*/public static String getDataSource() {return DATASOURCE_HOLDER.get();}/*** 删除当前数据源*/public static void removeDataSource() {DATASOURCE_HOLDER.remove();}}
核心类DynamicDataSource
package com.xunfeng.example.dynamic;import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.xunfeng.example.domain.entity.DataSourceEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import javax.sql.DataSource;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;/*** @author * @date 2024/6/17 14:19*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {private final static Map<Object, Object> targetDataSourceMap = new HashMap<>();public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {super.setDefaultTargetDataSource(defaultDataSource);super.setTargetDataSources(targetDataSources);targetDataSourceMap.put("master", defaultDataSource);
// targetDataSourceMap = targetDataSources;}@Overrideprotected Object determineCurrentLookupKey() {String dataSource = DataSourceContextHolder.getDataSource();DataSourceContextHolder.removeDataSource();return dataSource;}/*** 添加数据源信息** @param dataSources 数据源实体集合* @return 返回添加结果*/public Boolean createDataSource(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 = new DruidDataSource();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();return Boolean.TRUE;}return Boolean.FALSE;}/*** 校验数据源是否存在** @param key 数据源保存的key* @return 返回结果,true:存在,false:不存在*/public static boolean existsDataSource(Long key) {return Objects.nonNull(targetDataSourceMap.get(key));}public static Map<Object, Object> getTargetDataSourceMap() {return targetDataSourceMap;}}
核心类DynamicDataSourceConfig
这里主要功能为注册主数据源,也可以在这里注册更多的其他数据源
package com.xunfeng.example.dynamic;import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** @author * @date 2024/6/17 14:54*/
@Configuration
public class DynamicDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.dynamic.datasource.master")public DataSource masterDataSource(){return DruidDataSourceBuilder.create().build();}@Bean(name = "anotherDynamicDataSource")@Primarypublic DynamicDataSource dataSource() {Map<Object,Object> dataSourceMap = new HashMap<>();DataSource defaultDataSource = masterDataSource();dataSourceMap.put("master",defaultDataSource);return new DynamicDataSource(defaultDataSource,dataSourceMap);}
}
服务启动加载数据源类AnotherLoadDataSourceRunner
package com.xunfeng.example.dynamic.init;import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;import com.xunfeng.example.domain.entity.DataSourceEntity;
import com.xunfeng.example.dynamic.DynamicDataSource;
import com.xunfeng.example.mapper.DataSourceMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;/*** @author * @date 2024/6/17 14:31*/
@Component
public class AnotherLoadDataSourceRunner implements CommandLineRunner {@Autowiredprivate DataSourceMapper dataSourceMapper;@Resource(name = "anotherDynamicDataSource")private DynamicDataSource anotherDynamicDataSource;@Overridepublic void run(String... args) throws Exception {List<DataSourceEntity> list = dataSourceMapper.selectList(null);if (CollectionUtils.isNotEmpty(list)) {anotherDynamicDataSource.createDataSource(list);}}
}
自定义数据源切换注解DataSource
package com.xunfeng.example.dynamic.annotation;import java.lang.annotation.*;/*** @author * @date 2024/6/17 15:17*/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {String value() default "master";
}
切面DSAspect
package com.xunfeng.example.dynamic.aspect;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.xunfeng.example.domain.entity.DataSourceEntity;
import com.xunfeng.example.dynamic.DataSourceContextHolder;
import com.xunfeng.example.dynamic.annotation.DataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.util.Objects;/*** @author * @date 2024/6/17 15:18*/
@Aspect
@Component
public class DSAspect {@Pointcut("@annotation(com.xunfeng.example.dynamic.annotation.DataSource)")public void datasourcePoint() {}@Around("datasourcePoint()")public Object datasourceAround(ProceedingJoinPoint point) throws Throwable {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 instanceof DataSourceEntity) {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();}}
}