Springboot项目Druid运行时动态连接多数据源的功能
项目支持多数据库连接是个很常见的需求,这不仅是要在编译前连已经知道的多个数据库,有时还要在程序运行时连后期增加的多个数据源来获得数据。
一、编译前注册数据库连接
1.引入依赖包
<!-- springboot 3.x --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<!-- springboot 2.x --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId>
</dependency>
2.配置文件
一般本地应用可能有多个数据源,比如,需要支持读写分离的主从数据库配置,可能的application-druid.yml
配置文件代码如下:
# 数据源配置
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriverClassName: com.mysql.cj.jdbc.Driverdynamic:primary: MASTERdatasource:# 主库数据源MASTER:url: jdbc:mysql://127.0.0.1/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456# 从库数据源SLAVE:url: jdbc:mysql://127.0.0.1/ruoyi?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: 123456druid:# 初始连接数initialSize: 5# 最小连接池数量minIdle: 10# 最大连接池数量maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置连接超时时间connectTimeout: 30000# 配置网络超时时间socketTimeout: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒maxEvictableIdleTimeMillis: 900000# 配置检测连接是否有效validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsewebStatFilter:enabled: truestatViewServlet:enabled: true# 设置白名单,不填则允许所有访问allow:url-pattern: /druid/*# 控制台管理用户名和密码login-username: ruoyilogin-password: 123456filter:stat:enabled: true# 慢SQL记录log-slow-sql: trueslow-sql-millis: 1000merge-sql: truewall:config:multi-statement-allow: true
3.配置DruidConfig
/*** druid 配置多数据源* */
@Configuration
public class DruidConfig
{/** 主数据库配置 */@Bean@ConfigurationProperties("spring.datasource.druid.master")public DataSource masterDataSource(DruidProperties druidProperties){DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}/** 从数据库配置 */@Bean@ConfigurationProperties("spring.datasource.druid.slave")@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")public DataSource slaveDataSource(DruidProperties druidProperties){DruidDataSource dataSource = DruidDataSourceBuilder.create().build();return druidProperties.dataSource(dataSource);}/** 动态数据源 */@Bean(name = "dynamicDataSource")@Primarypublic DynamicDataSource dataSource(DataSource masterDataSource){Map<Object, Object> targetDataSources = new HashMap<>();// 加入主数据库配置targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);// 加入从数据库配置setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");return new DynamicDataSource(masterDataSource, targetDataSources);}/*** 设置数据源* * @param targetDataSources 备选数据源集合* @param sourceName 数据源名称* @param beanName bean名称*/public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName){try{DataSource dataSource = SpringUtils.getBean(beanName);targetDataSources.put(sourceName, dataSource);}catch (Exception e){}}
}
4.配置DynamicDataSource
新建DynamicDataSource
类来继承AbstractRoutingDataSource
类。
继承自 Spring 提供的 AbstractRoutingDataSource,通过重写 determineCurrentLookupKey 方法,根据上下文信息动态选择当前使用的数据源。
/*** 动态数据源类,用于实现多数据源切换。*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 构造函数,初始化默认数据源和目标数据源映射。** @param defaultTargetDataSource 默认数据源,在未指定其他数据源时使用* @param targetDataSourceMap 目标数据源映射,包含多个可选的数据源*/public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSourceMap) {// 设置默认数据源super.setDefaultTargetDataSource(defaultTargetDataSource);// 设置目标数据源映射super.setTargetDataSources(targetDataSourceMap);// 初始化父类属性super.afterPropertiesSet();}/*** 确定当前使用的数据源键。* 该方法会根据上下文中的数据源类型来决定使用哪个数据源。** @return 当前使用的数据源键*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.getDataSourceType();}}
5.配置DynamicDataSourceContextHolder
例如在web应用这种多线程的环境中,每个线程需要执行访问的数据库都不一样。因此需要给每个线程设置独立变量。
新建DynamicDataSourceContextHolder
文件,代码如下:
/*** 数据源切换处理* */
public class DynamicDataSourceContextHolder
{public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);/*** 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 设置数据源的变量*/public static void setDataSourceType(String dsType){log.info("切换到{}数据源", dsType);CONTEXT_HOLDER.set(dsType);}/*** 获得数据源的变量*/public static String getDataSourceType(){return CONTEXT_HOLDER.get();}/*** 清空数据源变量*/public static void clearDataSourceType(){CONTEXT_HOLDER.remove();}
}
6.配置AOP切面类
通过 AOP 拦截带有 @DataSource
注解的方法或类,在方法执行前设置数据源,在方法执行后清除数据源,确保每个请求都能正确使用指定的数据源。
代码如下:
/*** 多数据源处理切面类。*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect {protected Logger logger = LoggerFactory.getLogger(getClass());/*** 定义切入点,匹配带有 @DataSource 注解的方法或类。*/@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"+ "|| @within(com.ruoyi.common.annotation.DataSource)")public void dsPointCut() {}/*** 环绕通知,拦截 dsPointCut 切入点匹配的方法。* 在方法执行前设置数据源类型,在方法执行后清除数据源类型。** @param point 当前连接点对象* @return 方法返回值* @throws Throwable 如果方法执行过程中抛出异常*/@Around("dsPointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {DataSource dataSource = getDataSource(point);if (StringUtils.isNotNull(dataSource)) {// 根据注解配置设置数据源类型if ("".equals(dataSource.name())) {DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());} else {DynamicDataSourceContextHolder.setDataSourceType(dataSource.name());}}try {// 执行目标方法return point.proceed();} finally {// 方法执行完毕后清除数据源类型DynamicDataSourceContextHolder.clearDataSourceType();}}/*** 获取当前方法或类上的 @DataSource 注解配置。** @param point 当前连接点对象* @return 数据源配置信息*/public DataSource getDataSource(ProceedingJoinPoint point) {MethodSignature signature = (MethodSignature) point.getSignature();// 从方法上查找 @DataSource 注解DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);if (Objects.nonNull(dataSource)) {return dataSource;}// 如果方法上没有找到,则从类上查找 @DataSource 注解return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);}
}
7.使用方式
现在可以通过注解选择要使用哪个数据库。
在需要使用多数据源方法或类上添加@DataSource
注解,其中value用来表示数据源。
@DataSource(value = DataSourceType.SLAVE)
public List<SysUser> selectUserList(SysUser user)
{return userMapper.selectUserList(user);
}
@Service
@DataSource(value = DataSourceType.SLAVE)
public class SysUserServiceImpl{}
或者手动切换数据源
public List<SysUser> selectUserList(SysUser user)
{DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE.name());List<SysUser> userList = userMapper.selectUserList(user);DynamicDataSourceContextHolder.clearDataSourceType();return userList;
}
二、运行时访问数据库
前面访问多数据源的方式必须提前在yaml
这样的配置文件中提前填写好数据库的连接属性。
但是类似BI展示这样的软件项目可能得访问十几个业务的数据源,并且别的数据源的访问信息还时刻在变。这样的话,就不方便把数据库的连接属性提前写在配置文件中了。
1.配置数据管理工具
@Configuration
public class DataSourceManagement implements InitializingBean {protected final Logger logger = LoggerFactory.getLogger(DataSourceManagement.class);/*** 额外数据源*/private Map<String, DataSource> targetDataSources = new HashMap<>();/*** 配置文件的数据源*/@Autowiredprivate DynamicDataSourceProperties dataSourceProperties;@Autowiredprivate CreateDataSource c;@Autowired(required = false)private AfterCreateDataSource afterCreateDataSource;private DynamicDataSource dynamicDataSource;/*** 创建并返回动态数据源实例。** @return 动态数据源实例*/@Bean(name = "dynamicDataSource")@Primarypublic DynamicDataSource dataSource() {Map<Object, Object> targetDataSourceMap = new HashMap<>(targetDataSources);this.dynamicDataSource = new DynamicDataSource(targetDataSources.get(dataSourceProperties.getPrimary()), targetDataSourceMap);return this.dynamicDataSource;}/*** 验证数据源是否可用。** @param dataSource 要验证的数据源*/public void validateDataSource(DataSource dataSource) {try (Connection conn = dataSource.getConnection()) {String validationQuery = "SELECT 1";try (Statement stmt = conn.createStatement();ResultSet rs = stmt.executeQuery(validationQuery)) {if (!(rs.next() && rs.getInt(1) == 1)) {throw new RuntimeException("数据源连接验证失败:查询结果不正确");}}} catch (SQLException e) {throw new RuntimeException("数据源连接验证失败", e);}}/*** 在所有依赖项设置完成后执行此方法。* 初始化所有配置的数据源,并进行连接验证。*/@Overridepublic void afterPropertiesSet() throws Exception {dataSourceProperties.getDatasource().forEach((name, props) -> {Properties properties = dataSourceProperties.build(props);CommonDataSource commonDataSource = c.createDataSource(name, properties);if (afterCreateDataSource != null) {afterCreateDataSource.afterCreateDataSource(name, properties, commonDataSource);}DataSource dataSource = (DataSource) commonDataSource;logger.info("数据源:{} 校验中.......", name);// 计时long start = System.currentTimeMillis();validateDataSource(dataSource);logger.info("数据源:{} 链接成功,耗时:{}ms", name, System.currentTimeMillis() - start);this.putDataSource(name, dataSource);});}/*** 添加或更新一个数据源,并创建相应的SqlSessionFactory。** @param name 数据源名称* @param dataSource 数据源实例*/public void putDataSource(String name, DataSource dataSource) {targetDataSources.put(name, dataSource);}/*** 添加一个新的数据源。** @param name 数据源名称* @param properties 数据源属性*/public void addDataSourceWithCheck(String name, Properties properties) {if (targetDataSources.containsKey(name)) {logger.info("数据源" + name + "之前已经创建,准备测试数据源是否正常...");//throw new RuntimeException("数据源已存在");} else {CommonDataSource commonDataSource = c.createDataSource(name, properties);if (afterCreateDataSource != null) {afterCreateDataSource.afterCreateDataSource(name, properties, commonDataSource);}DataSource dataSource = (DataSource) commonDataSource;logger.info("数据源:{} 校验中.......", name);// 计时long start = System.currentTimeMillis();validateDataSource(dataSource);logger.info("数据源:{} 链接成功,耗时:{}ms", name, System.currentTimeMillis() - start);this.putDataSource(name, dataSource);HashMap<Object, Object> objectHashMap = new HashMap<>(this.targetDataSources);// 将map赋值给父类的TargetDataSourcesthis.dynamicDataSource.setTargetDataSources(objectHashMap);// 重要:将TargetDataSources中的连接信息放入resolvedDataSources管理,否则无法切换数据源this.dynamicDataSource.afterPropertiesSet();logger.debug("添加数据源成功,名称:{}", name);}}}
2.创建数据源
通过 DruidXADataSource 创建数据源,并使用 DruidConfig 和 DynamicDataSourceProperties 配置数据源属性。
/*** 数据源创建类,实现 CreateDataSource 接口。* */
@Component
public class DataSourceCreate implements CreateDataSource {@Autowiredprivate DynamicDataSourceProperties properties;@Autowiredprivate DruidConfig druidConfig;/*** 创建数据源的方法。** @param name 数据源名称* @param prop 数据源配置属性* @return 创建的数据源对象*/@Overridepublic DataSource createDataSource(String name, Properties prop) {// 创建 DruidXADataSource 实例DruidXADataSource dataSource = new DruidXADataSource();// 将创建的数据源添加到 DruidConfig 的数据源列表中druidConfig.getDruidDataSources().add(dataSource);// 设置数据源的连接属性dataSource.setConnectProperties(prop);// 使用 DynamicDataSourceProperties 配置数据源的其他属性properties.setProperties(dataSource, prop);// 返回创建的数据源对象return dataSource;}
}
3.准备实体
准备个存储数据库连接属性的实体,用于接收信息。
@Data
public class DataSource {private String id;private String connIp;private String connPort;private String connDbName;private String connUserName;private String connPassWord;private String connName;private String connType;private String connProperty;private String connDriverClass;
}
4.调用方式
写一个服务类,将从某个存储数据库连接信息的表里查出所有数据源。然后根据连接属性创建动态数据源。
@Service
public class DBChangeServiceImpl implements IDBChangeService {private final Logger logger = LoggerFactory.getLogger(DBChangeServiceImpl.class);@AutowiredDataSourceMapper dataSourceMapper;@Autowiredprivate DataSourceManagement dataSourceManagement;/*** 获取数据源* @return*/@Overridepublic List<DataSource> get() {return dataSourceMapper.get();}/*** 切换数据源* @param datasourceId* @return*/@Overridepublic boolean changeDb(String datasourceId) {//清除本线程的数据源DynamicDataSourceContextHolder.clearDataSourceType();logger.debug("清除本线程的数据源");//获取所有数据源List<DataSource> dataSourcesList = dataSourceMapper.get();logger.debug("获取到的数据源列表: {}", JSON.toJSONString(dataSourcesList));//遍历所有数据源,切换到指定数据源for (DataSource dataSource : dataSourcesList) {if (dataSource.getId().equals(datasourceId)) {logger.info(JSON.toJSONString(dataSource));logger.info("需要使用的的数据源已经找到,datasourceId是:" + dataSource.getId());Properties properties = buildDataSourceProperties(dataSource);//创建数据源连接&检查 若存在则不需重新创建try {// 使用DataSourceManagement来创建数据源并检查dataSourceManagement.addDataSourceWithCheck(dataSource.getId(), properties);// 切换数据源DynamicDataSourceContextHolder.setDataSourceType(dataSource.getId());logger.info("标记切换到数据源:"+datasourceId);return true;} catch (Exception e) {logger.error("数据源切换失败", e);e.printStackTrace();}}}logger.debug("未找到目标数据源");return false;}private Properties buildDataSourceProperties(DataSource dataSource) {Properties properties = new Properties();properties.setProperty("url", "jdbc:mysql://" + dataSource.getConnIp() + ":" + dataSource.getConnPort() + "/" + dataSource.getConnDbName());properties.setProperty("username", dataSource.getConnUserName());properties.setProperty("password", dataSource.getConnPassWord());properties.setProperty("driverClassName", dataSource.getConnDriverClass());// 添加其他必要的属性return properties;}}
切换数据源执行sql语句后,记得把数据源再切回去。
public AjaxResult runWithSql(String datasourceid, String sql){//切换到数据库dbChangeService.changeDb(datasourceid);System.out.println("Current DataSource Type: " + DynamicDataSourceContextHolder.getDataSourceType());System.out.println("RunWithSql: "+sql);List<Map<String, Object>> maps = execSqlService.runWithSql(sql);System.out.println("=======>"+JSON.toJSONString(maps));//切回主数据源DynamicDataSourceContextHolder.clearDataSourceType();return AjaxResult.success(maps);}
可以自行测试,发现连接成功了。