Java,SpringBoot实现动态切换数据源,数据源信息来源:yml+MySQL。动态加载时机自定义。切换时机方法调用前后,各线程分别切换。
别的动态切换已经很多了。我这里的需求是用户可选择数据源。
不废话,上代码:
首先application.yml中配置主数据库,配置通用即可。没有影响。这个主数据库中包换了其他数据源的配置信息。需要保存的信息如下:
连接方式可选择默认hikariCP,可以选择其他连接如druid,其他连接方式构造连接对象的代码需自行查找。
禁用SpringBoot的数据源自动配置
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
因为我们需要一个@Configration,用于在继承SpringBoot默认连接对象前,显式的加载配置,这样我们才可以直接使用这个连接对象做些事。
@Configuration
public class DynamicDataSourceConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
// 加载默认连接对象
@Bean
public DataSource firstDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setDriverClassName(driverClassName);
return new HikariDataSource(config);
}
// 如果其他连接对象是固定,也可以通过类似默认对象一样的方式全部加载进来
...
// 加载连接池,将默认连接对象初始化进去
@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource) {
return new DynamicDataSource(firstDataSource);
}
}
和一个继承了AbstractRoutingDataSource的类,在其中操作连接对象,包括默认连接对象,以及我们要加载的其他连接对象。
// 连接池操作
// 首先提供一个初始化方法,加默认配置显示的加载进来,重点是显示的保存这个连接池对象,方便后续操作切换
// 其次覆盖determineCurrentLookupKey方法,使我们可随时切换连接对象
public class DynamicDataSource extends AbstractRoutingDataSource {
// 连接池对象
private final Map<Object, Object> targetDataSourceMap;
// 加载默认连接,并将连接池对象保存到这里
public DynamicDataSource(Object firstDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", firstDataSource);
// 设置默认连接
super.setDefaultTargetDataSource(firstDataSource);
// 设置连接池
super.setTargetDataSources(targetDataSources);
// 刷新配置,使连接池生效
super.afterPropertiesSet();
this.targetDataSourceMap = targetDataSources;
}
// 从当前线程参数中获取要切换的数据源并设置到连接池中
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource() != null ? DataSourceContextHolder.getDataSource() : "master";
}
// 添加数据源
// 需要注意的是,该方法的调用时机,否则可能出现还没有添加数据源,就尝试切换数据源的问题
public void addDataSource(List<DataSourceDto> dataSources) {
if (dataSources != null && !dataSources.isEmpty()) {
for (int i = 0; i < dataSources.size(); i++) {
String key = "slave-" + dataSources.get(i).getId();
if (!dataSources.contains(key)) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dataSources.get(i).getUrl());
config.setUsername(dataSources.get(i).getUsername());
config.setPassword(dataSources.get(i).getPassword());
config.setDriverClassName(dataSources.get(i).getDriverClassName());
targetDataSourceMap.put(key, new HikariDataSource(config));
// 添加后刷新配置,也可以全部更新后一起刷新
super.afterPropertiesSet();
}
}
}
}
}
每个线程需要独立持有当前线程使用的连接对象的key。因此需要一个线程对象操作类。这里和其他的动态数据源切换中的持有方式都是一摸一样的。
// 设置当前线程数据源连接对象持有对象,每个线程独立使用对应的连接对象,只是保存作用,需要连接池自行取用
public class DataSourceContextHolder {
// 线程内容
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();
}
}
切换方法可以直接调用,也可以以注解的方式在需要切换数据源的方法前后切换数据源,在需要切换的方法上使用注解@DS即可。以用户选择使用数据源的情况为例,注解需要写为根据参数设置数据源:
// 使用有多种方式,可以主动调用DataSourceContextHolder的设置方法,也可以使用注解的方式解耦代码
@Aspect
@Order(1)
@Component
public class DBAspect {
@Pointcut("@annotation(com.md.db.annotation.DS)")
public void pointcut() {};
@Around("pointcut()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
Object[] args = jp.getArgs();
String[] paramNames = ((CodeSignature)jp.getSignature()).getParameterNames();
// 方法调用前切换数据源
for (int i = 0; i < paramNames.length; i++) {
if ("dsId".equals(paramNames[i])) {
DataSourceContextHolder.setDataSource("slave-" + args[i]);
break;
}
}
try {
return jp.proceed();
} finally {
// 方法调用后返回默认数据源,否则会影响不需要切换数据源而没有加注解的方法
DataSourceContextHolder.removeDataSource();
}
}
}
其他情况自行修改代码。
至于动态添加数据库的时机,我会在项目启动后去添加动态连接对象。也可以在前端用户到了数据源切换页面发送请求后再加载数据源,只要保证在其他需要切换数据源的方法前调用即可。
@Resource
private DynamicDataSource dynamicDataSource;
// 添加数据源
List<DataSourceDto> sourceList = this.dbMapper.getSourceList();
this.dynamicDataSource.addDataSource(sourceList);
当然,以上方法仅是demo,实际工作中还有许多问题,比如动态数据源删除,更新等等。就自行思考修改吧。