Spring MVC 多租户动态数据源切换需结合拦截器与AbstractRoutingDataSource实现:先通过拦截器解析租户ID并存入ThreadLocal,再由路由数据源根据租户ID选择对应DataSource。

Spring MVC 本身不直接管理数据源切换,多租户下的动态数据源切换需结合拦截器(HandlerInterceptor)+ 动态数据源路由(如 AbstractRoutingDataSource)实现。核心思路是:在请求进入 Controller 前,通过拦截器解析租户标识(如域名、请求头、URL 路径等),将其绑定到线程上下文(如 ThreadLocal),再由数据源路由类据此决定使用哪个 DataSource。
1. 定义租户识别方式并提取租户 ID
拦截器第一件事是识别当前请求属于哪个租户。常见方式有:
- 子域名识别:如 tenant1.example.com → 租户 tenant1;可用 HttpServletRequest.getServerName() 提取并截取
- 请求头携带:如 X-Tenant-ID: tenant2;用 request.getHeader("X-Tenant-ID") 获取
- 路径前缀识别:如 /tenant3/api/users → 租户 tenant3;用 request.getRequestURI() 解析路径段
- Token 解析(JWT):若已做统一认证,可从 JWT payload 中读取 tenant_id 字段
建议统一将租户 ID 存入自定义的 TenantContext 工具类中,内部用 ThreadLocal 管理:
public class TenantContext {private static final ThreadLocal<String> currentTenant = ThreadLocal.withInitial(() -> null);
public static void setTenant(String tenantId) { currentTenant.set(tenantId); }
public static String getTenant() { return currentTenant.get(); }
public static void clear() { currentTenant.remove(); }
}
2. 实现拦截器设置租户上下文
编写一个实现 HandlerInterceptor 的类,在 preHandle 中解析并设置租户 ID,afterCompletion 中清理 ThreadLocal 避免内存泄漏:
立即学习“Java免费学习笔记(深入)”;
public class TenantInterceptor implements HandlerInterceptor {@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = resolveTenantId(request);
if (tenantId == null || tenantId.trim().isEmpty()) {
throw new IllegalArgumentException("Tenant ID is required");
}
TenantContext.setTenant(tenantId);
return true;
}
private String resolveTenantId(HttpServletRequest request) {
// 示例:从 header 获取
return request.getHeader("X-Tenant-ID");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
TenantContext.clear();
}
}
注册该拦截器到 WebMvcConfigurer:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TenantInterceptor())
.excludePathPatterns("/actuator/**", "/health", "/login");
}
}
3. 构建动态数据源路由
继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法,返回当前租户 ID:
public class TenantRoutingDataSource extends AbstractRoutingDataSource {@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getTenant(); // 返回 tenantId 字符串
}
}
配置多个实际数据源(如 HikariCP),并注入到路由数据源中:
@Configurationpublic class DataSourceConfig {
@Bean
@Primary
public DataSource routingDataSource(@Qualifier("tenant1DataSource") DataSource t1,
@Qualifier("tenant2DataSource") DataSource t2) {
TenantRoutingDataSource routing = new TenantRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("tenant1", t1);
targetDataSources.put("tenant2", t2);
routing.setTargetDataSources(targetDataSources);
routing.setDefaultTargetDataSource(t1); // 默认租户
routing.afterPropertiesSet();
return routing;
}
@Bean("tenant1DataSource")
@ConfigurationProperties("spring.datasource.tenant1")
public DataSource tenant1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean("tenant2DataSource")
@ConfigurationProperties("spring.datasource.tenant2")
public DataSource tenant2DataSource() {
return DataSourceBuilder.create().build();
}
}
注意:application.yml 中需为每个租户配置独立连接池参数,例如:
spring:datasource:
tenant1:
jdbc-url: jdbc:mysql://localhost:3306/db_tenant1
username: user1
password: pwd1
tenant2:
jdbc-url: jdbc:mysql://localhost:3306/db_tenant2
username: user2
password: pwd2
4. 注意事项与增强点
该方案虽简洁,但需关注几个关键细节:
- 事务失效风险:Spring @Transactional 在数据源切换后可能无法正确传播,建议确保切面执行顺序早于事务拦截器(可通过 Ordered 接口或 @Order 注解控制)
- 连接池隔离:每个租户数据源应使用独立连接池(如 HikariCP),避免连接混用和泄露
- 租户校验前置:可在拦截器中增加租户合法性检查(如查库确认租户是否存在、是否启用)
- SQL 安全隔离:动态数据源只解决物理隔离,逻辑上仍需确保各租户查询不越权(如 MyBatis 中 SQL 不拼接租户字段,而靠数据源自动隔离)
- 异步调用问题:ThreadLocal 不会自动传递到子线程,若使用 CompletableFuture 或 @Async,需手动透传租户上下文
不复杂但容易忽略

















