SpringBoot多数据源功能封装
功能概述
本文详细阐述如何基于SpringBoot快速构建并封装多数据源的动态切换能力,实现灵活、可扩展的数据访问架构。
该方案具备以下核心优势:
- 双模式数据源初始化:支持通过配置文件或数据库表两种方式完成数据源的初始化加载。
- 多种切换策略支持:提供基于AOP注解、用户身份标识以及指定数据源名称等多种动态切换机制。
- 统一事务管理:通过DynamicDataSource进行全局数据源调度,并结合@Transactional(rollbackFor = Exception.class)保障跨数据源操作的事务一致性(注意:分布式事务需额外处理)。
- 轻量级配置部署:相比常见开源实现,本方案结构更清晰、配置更简洁、代码侵入性更低。
核心功能详解
1. 数据源初始化机制
系统支持两种初始化方式,可根据实际部署环境自由选择。方式一:通过配置文件初始化
当application.yml中spring.dynamic-datasource.from设置为CONF时,系统将读取datasource-list中的静态配置来创建多个数据源。示例如下:
spring:
dynamic-datasource:
# DB-从数据库读取, CONF-从配置文件读取
from: CONF
# 初始化数据源列表
datasource-list:
- name: h2-1
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test
username: h2
password: h2
- name: derby-1
driver-class-name: org.apache.derby.jdbc.EmbeddedDriver
url: jdbc:derby:derbyDb;create=true
username: derby
password: derby
方式二:通过数据库表动态加载
若from值设为DB,则系统会执行datasource-sql所定义的SQL语句,从主库中查询数据源信息并动态构建数据源池。配置参考如下:
spring:
dynamic-datasource:
from: DB
datasource-sql: SELECT id AS name, driver_class_name AS driverClassName, url, username, password FROM t_datasource
注意:SQL查询结果需包含name、driverClassName、url、username、password等关键字段,字段映射应与实体属性匹配。
2. 动态数据源核心类实现
定义一个继承自HikariDataSource的DynamicDataSource类,用于统一管理所有子数据源,并实现运行时动态切换逻辑。具体代码如下:
/**
* @author tony
* @desc 自定义HikariDataSource多数据源类,通过DynamicDataSourceKeyHolder动态设置运行时数据源key
* @date 2025/11/12 08:51
*/
public class DynamicDataSource extends HikariDataSource implements InitializingBean {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);
/**
* 存储所有可切换的数据源实例
*/
private final Map<String, HikariDataSource> dynamicDataSourceMap = new ConcurrentHashMap<>();
@Autowired
private DynamicDataSourceProperties dataSourceConfig;
public DynamicDataSource() {
super();
}
public DynamicDataSource(HikariConfig configuration) {
super(configuration);
}
/**
* 获取当前应使用的数据源
* 若线程上下文中已设置key,则使用对应数据源;否则使用默认数据源
*
* @return DataSource 实例
*/
public DataSource getDataSource() throws SQLException {
String key = DynamicDataSourceKeyHolder.getKey();
DataSource dataSource;
if (key == null) {
dataSource = this;
} else {
dataSource = dynamicDataSourceMap.get(key);
if (dataSource == null) {
dataSource = this; // 回退到默认数据源
}
}
return dataSource;
}
// ... 其他初始化及销毁逻辑省略
}
该类通过DynamicDataSourceKeyHolder工具类维护当前线程的数据源标识,从而实现精准的运行时路由控制。
private HikariDataSource createIfNotExistsDataSource(DataSourceProperties properties) throws SQLException {
String key = getKey(properties);
HikariDataSource dataSource = dynamicDataSourceMap.get(key);
if (dataSource == null) {
synchronized (dynamicDataSourceMap) {
if (dynamicDataSourceMap.get(key) == null) {
dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
dataSource.setLoginTimeout(this.getLoginTimeout());
dataSource.setValidationTimeout(this.getValidationTimeout());
dataSource.setConnectionTimeout(this.getConnectionTimeout());
dataSource.setIdleTimeout(this.getIdleTimeout());
dataSource.setMaxLifetime(this.getMaxLifetime());
dataSource.setMaximumPoolSize(this.getMaximumPoolSize());
dataSource.setMinimumIdle(this.getMinimumIdle());
dynamicDataSourceMap.put(key, dataSource);
} else {
dataSource = dynamicDataSourceMap.get(key);
}
}
}
return dataSource;
}
/**
* 获取数据源key
*
* @param properties 数据源信息
* @return 数据源key
*/
private static String getKey(DataSourceProperties properties) {
return properties.getName();
}
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
if (dataSource == null) {
throw new SQLException("未找到和[" + key + "]匹配的数据源。");
}
return dataSource;
/**
* 如果不存在,添加一个新数据源
*
* @param properties 数据连接信息
* @return 数据源
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
/**
* 创建数据源
*
* @param properties 数据源信息
* @param type 数据源类型
* @param <T> 数据源类型
* @return 数据源
*/
@Override
public Connection getConnection() throws SQLException {
DataSource dynamicDataSource = this.geDataSource();
if (this.equals(dynamicDataSource)) {
return super.getConnection();
}
return dynamicDataSource.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
DataSource dynamicDataSource = this.geDataSource();
if (this.equals(dynamicDataSource)) {
return super.getConnection(username, password);
}
return dynamicDataSource.getConnection(username, password);
}
/**
* 更新所有动态数据源
*/
public void updateAllDynamicDataSource() {
this.updateDynamicDataSource(getAllDatasourceList());
}
/**
* 根据新的数据源列表更新动态数据源配置
*
* @param newDatasourceList 新的数据源配置列表
*/
public void updateDynamicDataSource(List<DataSourceProperties> newDatasourceList) {
if (newDatasourceList == null) {
// 清理已存在的数据源
dynamicDataSourceMap.values().forEach(HikariDataSource::close);
dynamicDataSourceMap.clear();
} else {
// 遍历新列表,创建并添加未存在的数据源
newDatasourceList.forEach(row -> {
try {
dynamicDataSourceMap.putIfAbsent(getKey(row), createIfNotExistsDataSource(row));
} catch (SQLException e) {
LOGGER.error("DynamicDataSource init datasource exception:{}", e.getMessage(), e);
}
});
// 删除不再使用的旧数据源
if (!dynamicDataSourceMap.isEmpty()) {
Set<String> currentKeys = newDatasourceList.stream()
.map(DynamicDataSource::getKey)
.collect(Collectors.toSet());
Set<String> existingKeys = dynamicDataSourceMap.keySet();
for (String key : existingKeys) {
if (!currentKeys.contains(key)) {
dynamicDataSourceMap.remove(key).close();
}
}
}
}
}
/**
* 获取当前系统中所有的数据源配置列表
*
* @return 包含所有数据源属性的列表
*/
public List<DataSourceProperties> getAllDatasourceList() {
List<DataSourceProperties> datasourceAllList = new LinkedList<>();
String datasourceSql = dataSourceConfig.getDatasourceSql();
EDatasourceFrom from = dataSourceConfig.getFrom();
List<DataSourceProperties> datasourceList = dataSourceConfig.getDatasourceList();
if (from != null) {
switch (from) {
case DB:
NamedParameterJdbcTemplate jdbcTemplate = new NamedParameterJdbcTemplate(this);
在完成数据源的初始化后,执行如下操作:
datasourceAllList = jdbcTemplate.query(datasourceSql, new HashMap<>(), new BeanPropertyRowMapper<>(DataSourceProperties.class));
break;
case CONF:
if (datasourceList != null) {
datasourceAllList.addAll(datasourceList);
}
break;
}
}
return datasourceAllList;
}
@Override
public void afterPropertiesSet() {
this.updateAllDynamicDataSource();
LOGGER.info("DynamicDataSource init, datasource.size:{}", this.dynamicDataSourceMap.size());
}
接下来定义一个配置类 DynamicDataSourceProperties,用于获取所有数据源的信息,并通过 DataSourceConfig 完成动态数据源的初始化工作。
关于数据源的切换机制,采用 AOP 结合自定义注解的方式实现。
1. 自定义注解定义
创建一个名为 DynamicDatasource 的注解,用于标识需要切换数据源的方法或类。
/**
* @author tony
* @desc 动态数据源注解
* @date 2025/10/27 13:06
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicDatasource {
/**
* 数据源的唯一标识key
*/
String value() default "";
}
2. 切面逻辑实现
构建切面类 DynamicDatasourceAspect,用于拦截带有 DynamicDatasource 注解的方法,提前设置对应的数据源。
/**
* @author tony
* @desc 数据源切面处理类
* 注意:必须确保该切面在 Spring 默认事务拦截器(TransactionInterceptor)之前执行。
* 若事务拦截器先执行,则会使用默认数据源建立连接,导致切换失效。
* Spring 中事务管理器的默认顺序由 EnableTransactionManagement 指定,为 Ordered.LOWEST_PRECEDENCE。
* 因此此处设置优先级为 LOWEST_PRECEDENCE - 1,以保证先于事务拦截器触发。
* @date 2025/10/27 12:51
*/
@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1)
public class DynamicDatasourceAspect {
/**
* 在目标方法执行前,根据注解设置对应的数据源
*/
@Before("execution(* com.tony.jdbc.dynamic.service.*.*(..)) || execution(* com.tony.jdbc.dynamic.controller.*.*(..))")
public void setDatasource(JoinPoint joinPoint) {
System.out.println("Before execution");
// 获取方法签名和对应方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 优先检查方法上是否有注解
DynamicDatasource annotation = method.getAnnotation(DynamicDatasource.class);
// 若方法上无注解,则尝试获取类级别的注解
if (annotation == null) {
Object target = joinPoint.getTarget();
Class<?> targetClass = target.getClass();
annotation = targetClass.getAnnotation(DynamicDatasource.class);
}
// 如果存在注解,则将指定的数据源 key 设置到上下文中
if (annotation != null) {
DynamicDataSourceKeyHolder.setKey(annotation.value());
}
}
/**
* 目标方法执行完成后,清理当前线程中的数据源上下文
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册动态数据源拦截器,用于在Web请求中根据注解或用户信息切换数据源
registry.addInterceptor(new DynamicDatasourceInterceptor())
.addPathPatterns("/**") // 拦截所有请求路径
.order(1); // 设置拦截器优先级
}
}
通过实现 WebMvcConfigurer 并注册自定义拦截器 DynamicDatasourceInterceptor,可以在请求进入 Controller 之前进行数据源的动态切换。该方式主要适用于基于 Web 层的请求控制,若需覆盖 Service 层,则应使用切面(Aspect)方式进行统一管理。
以下为动态数据源拦截器的具体实现:
/**
* web服务Controller中数据源的动态拦截处理类。
* 支持通过类级别或方法级别的注解指定数据源,其中方法注解优先于类注解生效;
* 同时预留扩展机制,支持根据访问用户等上下文信息动态选择数据源。
*
* @author tony
* @date 2025/10/26 19:02
*/
public class DynamicDatasourceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 判断处理器是否为HandlerMethod类型,确保是Controller中的方法调用
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法上的@DynamicDatasource注解
DynamicDatasource methodAnnotation =
handlerMethod.getMethod().getAnnotation(DynamicDatasource.class);
// 获取控制器类上的@DynamicDatasource注解
DynamicDatasource classAnnotation =
handlerMethod.getBeanType().getAnnotation(DynamicDatasource.class);
// 若方法或类上存在注解,则设置对应的数据源key
if (methodAnnotation != null || classAnnotation != null) {
String datasourceKey = methodAnnotation != null ?
methodAnnotation.value() : classAnnotation.value();
DynamicDataSourceKeyHolder.setKey(datasourceKey);
return true;
}
}
/*
* TODO: 可在此处扩展其他数据源切换策略,
* 例如从请求头、会话或Token中提取用户身份信息,
* 根据不同用户分配不同的数据源。
*/
return true; // 继续执行后续处理链
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// 请求处理完成后、视图渲染前的操作(当前未做特殊处理)
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 请求结束后清理线程本地变量中的数据源标识
DynamicDataSourceKeyHolder.removeKey();
}
}
上述拦截器在请求预处理阶段(preHandle)根据注解内容设置当前线程所使用的数据源标识,并在请求完成之后(afterCompletion)及时清除该标识,防止内存泄漏或数据源错乱。
配合 AOP 切面技术,还可实现对 Service 层方法的监听与数据源切换:
@After("execution(* com.tony.jdbc.dynamic.service.*.*(..)) || " +
"execution(* com.tony.jdbc.dynamic.controller.*.*(..))")
public void removeDatasource() {
System.out.println("After execution");
DynamicDataSourceKeyHolder.removeKey();
}
此切面逻辑确保在 service 或 controller 方法执行完毕后自动清理数据源上下文,保障下一次调用不受影响。
总结:通过拦截器结合注解的方式,实现了按请求粒度灵活切换数据源的能力,既支持静态配置(如类/方法注解),也为动态规则(如按用户路由)提供了可扩展入口。
registry.addInterceptor(new DynamicDatasourceInterceptor())
.addPathPatterns("/**"); // 拦截所有Controller请求路径
通过数据源名称实现动态切换
为确保在事务中正确执行数据源的切换操作,同时简化使用过程中的复杂度,系统封装了名为“IDynamicService”的专用接口,用于统一管理多数据源环境下的操作。
IDynamicService 接口定义
该接口提供了多种针对不同模板工具的方法,支持无返回值执行、带结果返回以及更新操作等场景:
/**
* @author tony
* @desc JDBC动态数据源操作接口
* @date 2025/10/27 12:51
*/
public interface IDynamicService {
/**
* 使用指定数据源执行JdbcTemplate操作
*
* @param datasourceName 数据源名称
* @param consumer 具体执行逻辑
*/
void executeJdbc(String datasourceName, Consumer<JdbcTemplate> consumer);
/**
* 使用指定数据源执行并返回结果(基于JdbcTemplate)
*
* @param datasourceName 数据源名称
* @param function 执行逻辑及返回值处理
* @return 操作结果对象
*/
<T> T executeJdbcWithResult(String datasourceName, Function<JdbcTemplate, T> function);
/**
* 使用指定数据源执行NamedParameterJdbcTemplate操作
*
* @param datasourceName 数据源名称
* @param consumer 具体执行逻辑
*/
void executeParamJdbc(String datasourceName, Consumer<NamedParameterJdbcTemplate> consumer);
/**
* 使用指定数据源执行并返回结果(基于NamedParameterJdbcTemplate)
*
* @param datasourceName 数据源名称
* @param function 执行逻辑及返回值处理
* @return 操作结果对象
*/
<T> T executeParamJdbcWithResult(String datasourceName, Function<NamedParameterJdbcTemplate, T> function);
/**
* 执行更新类操作并返回影响行数(基于JdbcTemplate)
*
* @param datasourceName 数据源名称
* @param function 更新逻辑
* @return 影响的记录数量
*/
int updateJdbc(String datasourceName, Function<JdbcTemplate, Integer> function);
/**
* 执行参数化更新操作并返回影响行数(基于NamedParameterJdbcTemplate)
*
* @param datasourceName 数据源名称
* @param function 更新逻辑
* @return 影响的记录数量
*/
int updateParamJdbc(String datasourceName, Function<NamedParameterJdbcTemplate, Integer> function);
}
接口实现:DynamicServiceImpl
DynamicServiceImpl 是 IDynamicService 的具体实现类,采用 Spring 默认事务机制进行控制。其核心流程如下:
- 调用
DynamicDataSourceKeyHolder.setKey(datasourceName)设置当前线程的数据源标识; - 通过
transactionService.execute在事务上下文中执行业务逻辑; - 操作完成后,自动清理线程局部变量中的数据源标记,防止污染后续请求。
/**
* @author tony
* @desc 动态数据源服务实现类,集成Spring事务支持
* 步骤说明:
* 1. 设置目标数据源名称至上下文
* 2. 委托事务服务执行对应操作
* 3. 操作完毕后清除数据源上下文
* @date 2025/11/18 21:00
*/
@Service
public class DynamicServiceImpl implements IDynamicService {
private final ITransactionService transactionService;
private final JdbcTemplate jdbc;
private final NamedParameterJdbcTemplate namedParameterJdbc;
}
在动态数据源的实现中,事务管理是核心环节之一。通过引入transactionService,确保每项数据库操作都在事务上下文中执行,从而保障数据一致性与操作的原子性。
构造函数中注入了必要的组件:用于基础JDBC操作的JdbcTemplate、支持命名参数的NamedParameterJdbcTemplate,以及负责事务调度的ITransactionService。这些依赖共同支撑起多数据源环境下的执行逻辑。
针对不同的操作场景,提供了多个带事务控制的执行方法。例如,executeJdbc适用于无需返回结果的JDBC操作,通过回调接口接收JdbcTemplate实例并执行自定义逻辑。在调用前,先通过DynamicDataSourceKeyHolder.setKey(datasourceName)设置当前线程的数据源标识,确保路由正确。
对于需要获取执行结果的情形,使用executeJdbcWithResult方法,其接受一个函数式接口Function<JdbcTemplate, T>,并在事务中安全地返回泛型结果T。类似的设计也应用于命名参数模板的操作封装。
当涉及具名参数的SQL操作时,系统提供executeParamJdc和executeParamJdbcWithResult两个方法,分别处理无返回值和有返回值的场景,统一由namedParameterJdbc实例承载执行过程。
此外,更新操作如INSERT、UPDATE或DELETE可通过updateJdbc和updateParamJdbc完成,二者均返回受影响行数。它们同样运行在事务保护下,并基于传入的函数式逻辑进行数据库变更。
所有方法均遵循“设置数据源→执行事务→清除上下文”的模式,在finally块中调用DynamicDataSourceKeyHolder.removeKey()以防止线程局部变量泄漏,保证后续操作不受影响。
此类设计实现了对多数据源环境下事务与操作模板的透明化管理,提升了代码复用性和系统稳定性。
本示例中,仅需初始化一个名为“DynamicDataSource”的数据源。
当当前线程未指定数据源名称时,系统将自动使用默认数据源(例如:主业务数据库或公共配置信息库)。若当前线程已设置特定的数据源名称,则会根据该名称获取对应的数据连接。事务管理方面,统一采用 Spring 的事务注解 @Transactional(rollbackFor = Exception.class) 进行控制,确保操作的原子性与一致性。
测试接口配置
新增一个用于测试的 Controller,代码如下:
@RestController
@RequestMapping("base")
@DynamicDatasource("h2-1")
public class DynamicDatasourceController {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
IDynamicService dynamicService;
@Autowired
IDynamicAnnotationService dynamicAnnotationService;
获取所有数据源信息
通过以下接口可从默认数据源中查询出所有已注册的数据源配置信息:
/**
* 从默认数据源获取数据源信息
*
* @return 所有数据源的属性列表
*/
@GetMapping("getAllDatasource")
public List<DataSourceProperties> getAllDatasource() {
return namedParameterJdbcTemplate.query(
"select id,driver_class_name,url,username,password from t_datasource",
new HashMap<>(),
new BeanPropertyRowMapper<>(DataSourceProperties.class)
);
}
按数据源名称查询用户数据
该接口接收一个数据源名称作为参数,动态切换至对应数据源,并执行一系列 JDBC 操作:
- 创建临时用户表
t_user_test; - 插入一条测试记录,包含当前数据库产品名;
- 查询并返回插入的数据;
- 最后删除临时表;
- 清理当前线程中的数据源标识。
具体实现如下:
/**
* 根据数据源名称查询其用户表中的数据
*
* @param name 数据源名称
* @return 用户表中的数据映射
*/
@GetMapping("getByDatasourceName")
public Map<String, Object> getAllDatasource(String name) throws SQLException {
return dynamicService.executeJdbcWithResult(name, jdbcTemplate -> {
jdbcTemplate.execute("CREATE TABLE t_user_test(id varchar(225) not null,user_name varchar(225) not null,pwd varchar(225) not null,age int)");
String databaseProductName = null;
try {
databaseProductName = getDatabaseProduceName();
} catch (SQLException e) {
throw new RuntimeException(e);
}
jdbcTemplate.execute("INSERT INTO t_user_test VALUES ('0', '张三@" + databaseProductName + "','zs20', 20)");
Map<String, Object> userMap = namedParameterJdbcTemplate.queryForMap(("select id,user_name,pwd,age from t_user_test"), new HashMap<>());
jdbcTemplate.execute("drop TABLE t_user_test");
DynamicDataSourceKeyHolder.removeKey();
return userMap;
});
}
使用注解方式切换数据源
通过自定义注解 @DynamicDatasource 实现数据源的自动切换。以下方法演示了如何通过注解直接访问 H2 数据源并获取用户数据:
/**
* 使用注解方式切换到H2数据源,查询其用户表数据
*
* @return H2数据源中用户表的数据
*/
@GetMapping("getByAnnotationH2")
public Map<String, Object> getDataSourceH2() throws SQLException {
return dynamicAnnotationService.getDataSourceH2();
}
该方式无需手动传递数据源名称,由注解在方法级别声明目标数据源,提升代码可读性与维护性。
/**
* 通过注解实现数据源切换,用于查询derby数据源中的用户表信息
*
* @return 返回derby数据源中用户表的数据集合
*/
@GetMapping("getByAnnotationDerby")
public Map<String, Object> getDataSourceDerby() throws SQLException {
return dynamicAnnotationService.getDataSourceDerby();
}
/**
* 获取当前数据库的产品名称
*
* @return 数据库产品名称字符串
*/
private String getDatabaseProduceName() throws SQLException {
String databaseProductName = null;
Connection connection = null;
// 使用DataSourceUtils的getConnection方法获取连接,并通过doReleaseConnection安全释放连接,防止事务环境中连接被意外关闭
try {
connection = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
databaseProductName = connection.getMetaData().getDatabaseProductName();
} finally {
if (connection != null) {
DataSourceUtils.doReleaseConnection(connection, jdbcTemplate.getDataSource());
}
}
return databaseProductName;
}
访问接口示例:
查询默认数据源的数据:
请求地址:http://localhost:8080/test/base/getAllDatasource
根据指定名称查询不同数据源:
获取H2数据源实例数据:
http://localhost:8080/test/base/getByDatasourceName?name=h2-1
获取Derby数据源实例数据:
http://localhost:8080/test/base/getByDatasourceName?name=derby-1
使用注解方式进行数据源切换并查询:
切换至H2数据源执行查询:
http://localhost:8080/test/base/getByAnnotationH2
切换至Derby数据源执行查询:
http://localhost:8080/test/base/getByAnnotationDerby
总结
本文详细阐述了在SpringBoot项目中如何实现多数据源的动态切换功能。该方案配置简洁、集成方便,支持基于参数、名称以及注解等多种灵活的切换策略,能够适配多样化的业务需求场景。通过合理的设计与封装,有效降低了企业级应用中对多个数据库进行统一管理的技术复杂度,显著提升了开发效率和系统的整体稳定性。



雷达卡


京公网安备 11010802022788号







