Spring提供了AbstractRoutingDataSource,可以通过它实现动态数据源切换。你需要自定义一个DataSource路由器,根据当前选择的业务系统动态返回对应的数据源。
使用ThreadLocal来保存当前线程的数据源标识(如业务系统的ID或名称),在切换时更新ThreadLocal中的值。
你的配置已经定义了多个数据源(master、iss、eos、mabs),并且使用了Druid连接池。我们需要将这些数据源加载到Spring容器中,并通过AbstractRoutingDataSource实现动态切换。数据库相关.yaml配置文件内容如下
spring:
mvc:
date-format: yyyy-MM-dd HH:mm:ss
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
servlet:
multipart:
#开启文件上传
enabled: true
# 设置传输大小
max-file-size: 100MB
max-request-size: 100MB
datasource:
druid:
stat-view-servlet:
enabled: false
loginUsername: admin
loginPassword: 1a2b3c4d5e6!@#
allow:
web-stat-filter:
enabled: false
dynamic:
druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
# 连接池的配置信息
# 初始化大小,最小,最大
initial-size: 5
min-idle: 5
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
primary: master
datasource:
master:
url: jdbc:mysql://10.168.31.48:3306/nanjing_sjys_auth?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: root
password: postgres6666!
driver-class-name: com.mysql.cj.jdbc.Driver
iss:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=public&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
eos:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=eos&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
mabs:
url: jdbc:postgresql://10.168.31.48:5432/yunect_nanjing_iss?currentSchema=mabs&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8
username: postgres
password: postgres6666!
driver-class-name: org.postgresql.Driver
继承AbstractRoutingDataSource,实现动态数据源切换。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
使用ThreadLocal保存当前线程的数据源标识。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
将配置文件中的数据源加载到Spring容器中,并配置DynamicDataSource。
import com.alibaba.druid.pool.DruidDataSource;
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;
@Configuration
public class DataSourceConfig {
// 主数据源
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
// ISS 数据源
@Bean(name = "issDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.iss")
public DataSource issDataSource() {
return new DruidDataSource();
}
// ISS_EOS 数据源
@Bean(name = "eosDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.eos")
public DataSource issEosDataSource() {
return new DruidDataSource();
}
// ISS_MABS 数据源
@Bean(name = "mabsDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.mabs")
public DataSource issMabsDataSource() {
return new DruidDataSource();
}
// 动态数据源
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("issDataSource") DataSource issDataSource,
@Qualifier("issEosDataSource") DataSource issEosDataSource,
@Qualifier("issMabsDataSource") DataSource issMabsDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("iss", issDataSource);
targetDataSources.put("eos", issEosDataSource);
targetDataSources.put("mabs", issMabsDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认数据源
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.afterPropertiesSet();
return dynamicDataSource;
}
}
根据.yaml配置,Druid连接池已经启用。确保filters和connectionProperties等参数正确配置。
Service中的方法对每个数据源都通用,不关心具体的数据源。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DataService {
@Autowired
private DataRepository dataRepository;
/**
* 通用的查询方法
*/
public List<Data> getData() {
// 直接调用Repository方法,数据源由上层决定
return dataRepository.findAll();
}
/**
* 通用的插入方法
*/
public void saveData(Data data) {
dataRepository.save(data);
}
}
在Controller中根据用户传入的数据源名称动态设置数据源,并调用Service方法。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/data")
public class DataController {
@Autowired
private DataService dataService;
/**
* 根据数据源名称查询数据
*/
@GetMapping("/query")
public List<Data> queryData(@RequestParam String dataSourceKey) {
// 设置数据源
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
try {
// 调用Service方法
return dataService.getData();
} finally {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
/**
* 根据数据源名称保存数据
*/
@PostMapping("/save")
public void saveData(@RequestParam String dataSourceKey, @RequestBody Data data) {
// 设置数据源
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
try {
// 调用Service方法
dataService.saveData(data);
} finally {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
}
启动应用后,访问以下URL测试:
通过AOP在方法执行前切换数据源,方法执行后清除数据源上下文。@Order(-1)非常重要,会在后面详细介绍。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.modules.dataSource.DataSourceContextHolder;
import org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {
@Pointcut("@within(org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource) || @annotation(org.jeecg.modules.dataSource.aspect.annotation.SwitchDataSource)")
public void excudeService() {
}
@Before("excudeService()")
public void beforeSwitchDataSource(JoinPoint joinPoint) {
// 获取方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SwitchDataSource switchDataSource = method.getAnnotation(SwitchDataSource.class);
String dataSourceKey = null;
// 如果注解值不为空,使用注解值
if (switchDataSource != null && !switchDataSource.value().isEmpty()) {
dataSourceKey = switchDataSource.value();
}
// 如果注解值为空,从方法参数中获取数据源名称
else if (joinPoint.getArgs().length > 0 && joinPoint.getArgs()[0] instanceof String) {
dataSourceKey = (String) joinPoint.getArgs()[0];
}
// 设置数据源
if (dataSourceKey != null && !dataSourceKey.isEmpty()) {
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
}
}
@After("excudeService()")
public void afterClearDataSource(JoinPoint joinPoint) {
// 清除数据源上下文
DataSourceContextHolder.clearDataSourceKey();
}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value() default "master"; // 默认使用主数据源
}
将 @SwitchDataSource 注解加在 DataService 类上,并通过方法参数传入数据源名称。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@SwitchDataSource // 加在类上,表示该类的方法支持动态数据源切换
public class DataService {
@Autowired
private DataRepository dataRepository;
/**
* 通用的查询方法
*/
public List<Data> getData(String dataSourceKey) {
return dataRepository.findAll();
}
/**
* 通用的插入方法
*/
public void saveData(String dataSourceKey, Data data) {
dataRepository.save(data);
}
}
在Controller中调用Service方法,传入数据源名称。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/data")
public class DataController {
@Autowired
private DataService dataService;
/**
* 查询数据
*/
@GetMapping("/query")
public List<Data> queryData(@RequestParam String dataSourceKey) {
// 调用Service方法,传入数据源名称
return dataService.getData(dataSourceKey);
}
/**
* 保存数据
*/
@PostMapping("/save")
public void saveData(@RequestParam String dataSourceKey, @RequestBody Data data) {
// 调用Service方法,传入数据源名称
dataService.saveData(dataSourceKey, data);
}
}
启动应用后,访问以下URL测试:
@Order(-1)是 Spring AOP 中的一个注解,用于指定切面的执行顺序。
代码中,DataSourceAspect 切面用于动态切换数据源。为了确保数据源切换的逻辑在其他切面之前执行,使用了 @Order(-1)。这样可以:
确保每次操作后清除ThreadLocal中的数据源标识,避免线程复用导致数据源错乱。
如果涉及跨数据源的事务,需要使用分布式事务(如Seata)。
确保Druid连接池参数合理配置,避免频繁创建和销毁连接。
通过动态数据源切换可以实现多业务系统的数据隔离。关键点在于: