demo使用的时SpringBoot3.x、JDK17、MybatisPlus3.5.x、MySQL8
从数据中加载数据源
定义接口,指定数据源,从不同数据库获取数据
创建数据源表,用于指定不同数据源,程序自动动态获取
demo中所用到的工具以及版本号如下:
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.molu</groupId>
<artifactId>mgzyf-api</artifactId>
<version>2.4.1</version>
<description>基于 Java 17 + SpringBoot 3 。</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version> <!-- lookup parent from repository -->
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.15</hutool.version>
<mysql.version>8.0.28</mysql.version>
<druid.version>1.2.16</druid.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!--编译测试环境,不打包在lib-->
<scope>provided</scope>
</dependency>
<!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
<scope>provided</scope>
</dependency>
<!-- hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 阿里druid工具包 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.40</version>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 自定义配置类支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置两个数据源:
tets01
数据库和test02
数据库
默认使用master
主数据源,切换时只需要配置@DataSource
注解来切换即可
application.yml
server:
port: 8989
spring:
jackson:
## 默认序列化时间格式
date-format: yyyy-MM-dd HH:mm:ss
## 默认序列化时区
time-zone: GMT+8
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# 主数据源(test01数据库)
master:
url: jdbc:mysql://127.0.0.1:3306/test01?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: xxx
password: 123
driver-class-name: com.mysql.cj.jdbc.Driver
# 从数据源(test02数据库)
slave:
# 是否开启从库(多数据源情况下,请打开)
enable: true
# 如果时SQL server、Oracle等数据库,这里需要改成对应的驱动配置
url: jdbc:mysql://127.0.0.1:3306/test02?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: xxx
password: 123
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 15
min-idle: 15
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: ""
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
connection-properties: false
读取
application
配置文件中的数据源信息,并由Spring统一管理,以便后续使用
DataSourceConfig
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
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 com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
/**
* @author 陌路
* @apiNote 设置数据源
* @date 2023/12/31 10:16
* @tool Created by IntelliJ IDEA
*/
@Configuration
public class DataSourceConfig {
/*
通过配置类,将配置文件中配置的数据库信息转换成datasource,
并添加到DynamicDataSource中,同时通过@Bean将DynamicDataSource注入Spring中进行管理,
后期在进行动态数据源添加时,会用到。
*/
/**
* 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
*
* @return DataSource
* @description 该数据源是在application配置文件master中所配置的
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置从数据源,可以配置多个(目前只配置了一个test02)
*
* @return DataSource
* @description 该数据源是在application配置文件slave中所配置的
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置动态数据源的核心配置项
*
* @return DynamicDataSource
*/
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource createDynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
// 默认的数据源(主数据源)
DataSource defaultDataSource = masterDataSource();
// 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
dataSourceMap.put(DataSourceType.MASTER.name(), defaultDataSource);
// 配置从数据源,可以配置多个(目前只配置了一个test02)
dataSourceMap.put(DataSourceType.SLAVE.name(), slaveDataSource());
// 配置动态数据源,默认使用主数据源,如果有从数据源配,则使用从数据库中读取源,并加载到dataSourceMap中
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}
该枚举类主要用于数据源的指定,使用
MASTER
库还是使用SLAVE
库,
如果有多个数据源,可以在这里继续指定即可
DataSourceType
/**
* @author 陌路
* @apiNote 动态数据源类型
* @date 2023/12/31 10:23
* @tool Created by IntelliJ IDEA
*/
public enum DataSourceType {
// 注意:枚举项要和 DataSourceConfig 中的 createDynamicDataSource()方法dataSourceMap的key保持一致
/**
* 主库
*/
MASTER,
/**
* 从库
*/
SLAVE,
}
线程局部变量,不同于普通变量,每个线程都有自己的副本,互不影响。
多线程情况下也能保证数据源能够有序的切换,而不会造成数据的混乱
DynamicDataSourceContextHolder
import lombok.extern.slf4j.Slf4j;
/**
* @author 陌路
* @apiNote 线程局部变量,不同于普通变量,每个线程都有自己的副本,互不影响。
* @date 2023/12/31 10:05
* @tool Created by IntelliJ IDEA
* @description 创建一个类用于实现ThreadLocal,主要是通过get,set,remove方法来获取、设置、删除当前线程对应的数据源。
*/
@Slf4j
public class DynamicDataSourceContextHolder {
//此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();
/**
* 设置数据源
*
* @param dataSourceName 数据源名称
*/
public static void setDataSource(String dataSourceName) {
log.info("切换数据源到:{}", dataSourceName);
DATASOURCE_HOLDER.set(dataSourceName);
}
/**
* 获取当前线程的数据源
*
* @return 数据源名称
*/
public static String getDataSource() {
return DATASOURCE_HOLDER.get();
}
/**
* 删除当前数据源
*/
public static void removeDataSource() {
log.info("删除当前数据源:{}", getDataSource());
DATASOURCE_HOLDER.remove();
}
}
定义一个动态数据源类实现AbstractRoutingDataSource,通过determineCurrentLookupKey方法与上述实现的ThreadLocal类中的get方法进行关联,实现动态切换数据源。
切换数据源后,具体查询哪个数据库,由此类决定
DynamicDataSource
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
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.List;
import java.util.Map;
import java.util.Objects;
/**
* @author 陌路
* @apiNote 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
* @date 2023/12/31 10:08
* @tool Created by IntelliJ IDEA
* @description 定义一个动态数据源类实现AbstractRoutingDataSource,通过determineCurrentLookupKey方法与上述实现的ThreadLocal类中的get方法进行关联,实现动态切换数据源。
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/*
代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以Map保存的各种目标数据源。
其中Map的key是设置的数据源名称,value则是对应的数据源(DataSource)
*/
// 数据源列表,多数据源情况下,具体使用哪一个数据源,由此获取
private final Map<Object, Object> targetDataSourceMap;
/**
* 构造方法,设置默认数据源和目标多数据源
*
* @param defaultDataSource 默认主数据源,只能有一个
* @param targetDataSources 从数据源,可以是多个
*/
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
// 配置完成后,更新数据源配置列表,将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
}
/**
* 动态数据源的切换(核心)
* 决定使用哪个数据源
*
* @return Object
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
/**
* 校验数据源是否存在
*
* @param key 数据源保存的key
* @return 返回结果,true:存在,false:不存在
*/
public boolean existsDataSource(String key) {
return Objects.nonNull(this.targetDataSourceMap) && Objects.nonNull(this.targetDataSourceMap.get(key));
}
}
此注解可以配置在类上,也可以配置在方法上
优先级:方法 > 类
当方法上加了此注解时,类上配置的注解将会失效
当类中所有方法都没有此注解,而类上存在时,那么该类中所有方法都将使用该类上的数据源
方法和类上都不添加该注解时,默认使用master
数据源
DataSource
import java.lang.annotation.*;
/**
* 自定义多数据源切换注解
* <p>
* 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {
/**
* 切换数据源名称(默认是主数据源test01)
*/
public DataSourceType value() default DataSourceType.MASTER;
}
是否需要切换数据源,或者具体使用哪个数据源,由此类通过解析
DataSource
注解来决定
不添加或者解析不到该注解时,则使用默认的数据源(master)
DataSourceAspect
import org.apache.commons.lang3.StringUtils;
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.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.aspectj.lang.reflect.MethodSignature;
import java.util.Objects;
/**
* @author 陌路
* @apiNote 多数据源切换
* @date 2023/12/31 13:06
* @tool Created by IntelliJ IDEA
*/
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
// 配置织入点,为DataSource 注解
@Pointcut("@annotation(cn.molu.system.common.datasource.DataSource)"
+ "|| @within(cn.molu.system.common.datasource.DataSource)")
public void dsPointCut() {
}
/**
* * 环绕通知
*
* @param point 切入点
* @return Object
* @throws Throwable 异常
*/
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (Objects.nonNull(dataSource) && StringUtils.isNotEmpty(dataSource.value().name())) {
// 将用户自定义配置的数据源添加到线程局部变量中
DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());
}
try {
return point.proceed();
} finally {
// 在执行完方法之后,销毁数据源
DynamicDataSourceContextHolder.removeDataSource();
}
}
/**
* 获取需要切换的数据源
* 注意:顺序为:方法>类,方法上加了注解后类上的将不会生效
* 注意:当类上配置后,方法上没有该注解,那么当前类中的所有方法都将使用类上配置的数据源
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
// 从方法上获取注解
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
// 方法上不存在时,再从类上匹配
return Objects.nonNull(dataSource) ? dataSource : AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
}
上述中,我们自定义了数据源的配置,这里据需要将
DataSourceAutoConfiguration
自动配置的数据源排除,否则项目启动时可能会出现异常,导致项目无法正常运行
SystemApplication
// 排除数据源自动配置,使用自定义数据源,否则会出现循环依赖报错
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SystemApplication {
public static void main(String[] args) {
SpringApplication.run(SystemApplication.class, args);
log.info("项目启动成功:http://127.0.0.1:8989");
}
}
到此,配置类型的多数据源的切换就完成了
不足和缺陷:
- 每次添加数据源时都需要在
application
配置文件中添加- 线上环境时,每次都需要替换新的
application
文件- 当数据库密码修改或者切换服务器时,维护成本高,不够便利
以上不足和缺陷,将在以下内容中逐一解决
数据源信息配置在数据库中,便于维护
在以上代码的基础上,进行修改,所新增的代码以及变更的代码将会给出具体的说明
在以上代码的基础上新增:
SysDbInfo、LoadDataSourceRunner
【2个新增】
修改变更的类:DataSourceConfig、DynamicDataSource
【2个改动】
以下类不做任何改动:DynamicDataSourceContextHolder、DataSourceType、DataSourceAspect、DataSource、SystemApplication
【5个未作改动】
改动内容:仅去除了从数据库的配置信息(不再需要在配置文件中配置从库数据)
server:
port: 8989
spring:
jackson:
## 默认序列化时间格式
date-format: yyyy-MM-dd HH:mm:ss
## 默认序列化时区
time-zone: GMT+8
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/test01?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
username: xxx
password: 123
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
# 是否开启从库(多数据源情况下,请打开,并且将配置信息配置到sys_db_info表中,项目启动时会自动加载)
enable: true
initial-size: 15
min-idle: 15
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: ""
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false
connection-properties: false
SysDbInfo
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* @author 陌路
* @apiNote 数据源实体类
* @date 2023/12/31 10:29
* @tool Created by IntelliJ IDEA
*/
@Data
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@Accessors(chain = true)
@TableName(value = "sys_db_info")
public class SysDbInfo implements Serializable {
@Serial
@TableField(exist = false)
private static final long serialVersionUID = 8115921127536664152L;
/*
-- 建表语句
CREATE TABLE `sys_db_info` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库URL',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码',
`driver_class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库驱动',
`db_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库名称',
`db_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库key,即保存Map中的key(保证唯一,并且和DataSourceType中的枚举项保持一致,包括大小写)',
`status` int NOT NULL DEFAULT '0' COMMENT '是否停用:0-正常,1-停用',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注说明',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `UNI_DB_INFO_DBKEY` (`db_key`) USING BTREE COMMENT '数据库的key必须唯一,并且要和DataSourceType中的枚举项保持一致'
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
*/
/**
* 数据库地址
*/
private String url;
/**
* 数据库用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 数据库驱动
*/
private String driverClassName;
/**
* 数据库key,即保存Map中的key(保证唯一)
* 定义一个key用于作为DynamicDataSource中Map中的key。
* 这里的key需要和DataSourceType中的枚举项保持一致
*/
private String dbKey;
/**
* 数据库名称
*/
private String dbName;
/**
* 是否停用:0-正常,1-停用
*/
private Integer status;
/**
* 备注
*/
private String remark;
}
以下是实体类所对应的SQL语句:
CREATE TABLE `sys_db_info` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '主键Id',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库URL',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '密码',
`driver_class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库驱动',
`db_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库名称',
`db_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '数据库key,即保存Map中的key(保证唯一,并且和DataSourceType中的枚举项保持一致,包括大小写)',
`status` int NOT NULL DEFAULT '0' COMMENT '是否停用:0-正常,1-停用',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注说明',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `UNI_DB_INFO_DBKEY` (`db_key`) USING BTREE COMMENT '数据库的key必须唯一,并且要和DataSourceType中的枚举项保持一致'
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;
-- 添加数据库配置信息(此处按照自己的需求配置对应的数据库即可)
INSERT INTO `sys_db_info` (`id`, `url`, `username`, `password`, `driver_class_name`, `db_name`, `db_key`, `status`, `remark`) VALUES (1, 'jdbc:mysql://127.0.0.1:3306/test02?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true', 'xxx', '123', 'com.mysql.cj.jdbc.Driver', 'test02', 'SLAVE', 0, '连接test02数据库');
INSERT INTO `sys_db_info` (`id`, `url`, `username`, `password`, `driver_class_name`, `db_name`, `db_key`, `status`, `remark`) VALUES (2, 'jdbc:mysql://127.0.0.1:3306/test03?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true', 'xxx', '123', 'com.mysql.cj.jdbc.Driver', 'test03', 'SLAVE2', 1, '链接test03数据库');
项目启动时,执行此代码,访问数据库加载数据库中配置的数据源信息
LoadDataSourceRunner
import cn.hutool.core.util.StrUtil;
import cn.molu.system.mapper.SysDbInfoMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* @author 陌路
* @apiNote 查询数据源
* @date 2023/12/31 10:41
* @tool Created by IntelliJ IDEA
* CommandLineRunner 项目启动时执行
*/
@Slf4j
@Component
public class LoadDataSourceRunner implements CommandLineRunner {
/**
* 是否启用从库多数据源配置
*/
@Value("${spring.datasource.druid.slave.enable}")
private boolean enabled;
@Resource
private DynamicDataSource dynamicDataSource;
@Resource
private SysDbInfoMapper dbInfoMapper;
/**
* 项目启动时加载数据源
*/
@Override
public void run(String... args) {
if (enabled) return;
refreshDataSource();
}
/**
* 刷新数据源
*/
public void refreshDataSource() {
List<SysDbInfo> dbInfos = dbInfoMapper.selectList(new LambdaQueryWrapper<SysDbInfo>().eq(SysDbInfo::getStatus, 0));
if (CollectionUtils.isEmpty(dbInfos)) return;
List<SysDbInfo> ds = new ArrayList<>();
log.info("====开始加载数据源====");
for (SysDbInfo info : dbInfos) {
if (StrUtil.isAllNotBlank(
info.getUrl(), // 数据库连接地址
info.getDriverClassName(), // 数据库驱动
info.getUsername(), // 数据库用户名
info.getPassword(), // 数据库密码
info.getDbKey() // 数据源key
)) {
ds.add(info);
log.info("加载到数据源 ---> dbName:{}、dbKey:{}、remark:{}", info.getDbName(), info.getDbKey(), info.getRemark());
}
}
dynamicDataSource.createDataSource(ds);
log.info("====数据源加载完成====");
}
}
该配置类稍作改动,去除了从配置文件中加载从数据源
(slave)
,只加载主数据源(master)
DataSourceConfig
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
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 com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
/**
* @author 陌路
* @apiNote 设置数据源
* @date 2023/12/31 10:16
* @tool Created by IntelliJ IDEA
*/
@Configuration
public class DataSourceConfig {
/*
通过配置类,将配置文件中的配置的数据库信息转换成datasource,
并添加到DynamicDataSource中,同时通过@Bean将DynamicDataSource注入Spring中进行管理,
后期在进行动态数据源添加时,会用到。
*/
/**
* 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
*
* @return DataSource
* @description 该数据源是在application配置文件master中所配置的
*/
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置从数据源,可以配置多个
* <p>现在已经改为数据库配置,如果有多个数据源,请在sys_db_info表中添加即可,
* 不需要在application中配置(此方法已注释,配置也不会生效)<p/>
*
* @return DataSource
* @description 该数据源是在application配置文件slave中所配置的
*/
// @Bean
// @ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
/**
* 配置动态数据源的核心配置项
*
* @return DynamicDataSource
*/
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource createDynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>();
// 默认数据源
DataSource defaultDataSource = masterDataSource();
// 配置主数据源,默认使用该数据源,并且主数据源只能配置一个
dataSourceMap.put(DataSourceType.MASTER.name(), defaultDataSource);
// 配置从数据源,可以配置多个,目前已注释,不需要application中配置,如果有多个数据源,请在sys_db_info表中添加即可
// dataSourceMap.put(DataSourceType.SLAVE.name(), slaveDataSource());
// 配置动态数据源,默认使用主数据源,如果有从数据源配,则使用从数据库中读取源,并加载到dataSourceMap中
return new DynamicDataSource(defaultDataSource, dataSourceMap);
}
}
改动添加切换的数据源类,将数据库中读取出来的数据源列表逐一添加,以便后续使用
DynamicDataSource
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
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.List;
import java.util.Map;
import java.util.Objects;
/**
* @author 陌路
* @apiNote 实现动态数据源,根据AbstractRoutingDataSource路由到不同数据源中
* @date 2023/12/31 10:08
* @tool Created by IntelliJ IDEA
* @description 定义一个动态数据源类实现AbstractRoutingDataSource,通过determineCurrentLookupKey方法与上述实现的ThreadLocal类中的get方法进行关联,实现动态切换数据源。
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/*
代码中,还实现了一个动态数据源类的构造方法,主要是为了设置默认数据源,以及以Map保存的各种目标数据源。
其中Map的key是设置的数据源名称,value则是对应的数据源(DataSource)
*/
// 数据源列表,多数据源情况下,具体使用哪一个数据源,由此获取
private final Map<Object, Object> targetDataSourceMap;
/**
* 构造方法,设置默认数据源和目标多数据源
*
* @param defaultDataSource 默认主数据源,只能有一个
* @param targetDataSources 从数据源,可以是多个
*/
public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
this.targetDataSourceMap = targetDataSources;
// 配置完成后,更新数据源配置列表,将TargetDataSources中的连接信息放入resolvedDataSources管理
// super.afterPropertiesSet();
}
/**
* 动态数据源的切换(核心)
* 决定使用哪个数据源
*
* @return Object
*/
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
/**
* 添加数据源信息
*
* @param dataSources 数据源实体集合
*/
public void createDataSource(List<SysDbInfo> dataSources) {
try {
if (CollectionUtils.isNotEmpty(dataSources)) {
for (SysDbInfo ds : dataSources) {
//校验数据库是否可以连接
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();
// 将数据源放入Map中,key为数据源名称,要和DataSourceType中的枚举项对应,包括大小写,并且保证唯一
this.targetDataSourceMap.put(ds.getDbKey(), dataSource);
}
// 更新数据源配置列表,这里主要是从数据源
super.setTargetDataSources(this.targetDataSourceMap);
// 将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
}
} catch (ClassNotFoundException | SQLException e) {
log.error("---解析数据源出错---:{}", e.getMessage());
}
}
/**
* 校验数据源是否存在
*
* @param key 数据源保存的key
* @return 返回结果,true:存在,false:不存在
*/
public boolean existsDataSource(String key) {
return Objects.nonNull(this.targetDataSourceMap) && Objects.nonNull(this.targetDataSourceMap.get(key));
}
}
用于向数据库查询数据
import cn.molu.system.common.datasource.SysDbInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysDbInfoMapper extends BaseMapper<SysDbInfo> {}
到此,动态数据源相关就完成了,可以启动项目试一下
注意说明:
1、
master
主数据库一定要有,并且可以连接,否则项目无法启动
2、slave
从数据库可以有也可以没有,也可以时很多个,
3、slave
从数据库可以是其他数据库:Oracle、SQL serve等,对应修改配置即可
4、DataSourceType
枚举中的枚举项一定要和数据库中配置的dbkey
保持一致
5、使用的时候之间使用DataSource
注解指定数据源即可
动态数据源的使用
/**
* 用户控制器
*
* @author 陌路
* @since 2023/12/31
*/
@RestController
@RequiredArgsConstructor
// 此注解用于指定具体使用哪个数据源
@DataSource(DataSourceType.SLAVE)
@RequestMapping("/api/v1/users")
public class SysUserController {
private final SysUserService userService;
/**
* 用户分页列表
* @param queryParams 查询参数
* @return 用户分页列表
*/
@GetMapping("/page") // 使用类上的数据源SLAVE
public PageResult<UserPageVO> getUserPage(UserPageQuery queryParams) {
IPage<UserPageVO> result = userService.getUserPage(queryParams);
return PageResult.success(result);
}
/**
* 删除用户
*
* @param ids 用户ID,多个以英文逗号(,)分割
* @return 删除结果
*/
@DeleteMapping("/{ids}")
@DataSource(DataSourceType.MASTER) // 在方法上,则类上的配置失效
public Result<Boolean> deleteUsers(@PathVariable String ids) {
boolean result = userService.deleteUsers(ids);
return Result.judge(result);
}
}
不切换数据源,使用默认
不加@DataSource
注解,使用默认数据源
```java
/**
* 用户控制器
*
* @author 陌路
* @since 2023/12/31
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/users")
public class SysUserController {
private final SysUserService userService;
/**
* 用户分页列表
* @param queryParams 查询参数
* @return 用户分页列表
*/
@GetMapping("/page")
public PageResult<UserPageVO> getUserPage(UserPageQuery queryParams) {
IPage<UserPageVO> result = userService.getUserPage(queryParams);
return PageResult.success(result);
}
/**
* 删除用户
*
* @param ids 用户ID,多个以英文逗号(,)分割
* @return 删除结果
*/
@DeleteMapping("/{ids}")
public Result<Boolean> deleteUsers(@PathVariable String ids) {
boolean result = userService.deleteUsers(ids);
return Result.judge(result);
}
}
以上就是配置动态数据源的过程了,可以根据不同需求使用配置文件的形式,或者使用数据库配置的形式
我这里没有将测试过程以及结果贴出来,请自行测试使用。。。