
本文介绍如何在单个 war 包部署多个子域名(如 myapp1.mydomain.com)时,基于 http 请求的 host 头自动路由至对应数据库 schema,避免重复构建 war 文件,实现真正的运行时多租户数据隔离。
本文介绍如何在单个 war 包部署多个子域名(如 myapp1.mydomain.com)时,基于 http 请求的 host 头自动路由至对应数据库 schema,避免重复构建 war 文件,实现真正的运行时多租户数据隔离。
在 Spring Boot + Spring Data JPA 的典型 Web 应用中,若需通过同一套代码支持多个租户(如 myapp1.mydomain.com → schema1、myapp2.mydomain.com → schema2),最理想的方式不是打包多个 WAR 文件,而是在运行时根据请求上下文动态确定数据库 Schema。Hibernate 提供了 hibernate.default_schema 属性,可统一为所有实体指定默认 schema 前缀——关键在于让该值能随每次请求的 Host 动态变化。
但需注意:hibernate.default_schema 是 SessionFactory 级别配置,全局静态生效,无法按请求粒度变更。因此,正确做法是:在应用启动时暂不固化 schema,而是在每次数据库操作前,通过 CurrentTenantIdentifierResolver + 多数据源路由或动态方言适配实现租户感知。不过,对于“单库多 schema”(即同一 MySQL/PostgreSQL 实例下多个逻辑 schema)场景,更轻量且推荐的方案是结合 AbstractRoutingDataSource 与 ThreadLocal 上下文实现运行时数据源切换。
以下是完整实现步骤:
✅ 步骤一:定义租户识别器(基于 Host Header)
@Component
public class TenantContext {
private static final ThreadLocal<string> CURRENT_TENANT = new ThreadLocal();
public static void setCurrentTenant(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}</string>
✅ 步骤二:实现多数据源路由(支持 schema 切换)
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
✅ 步骤三:配置数据源 Bean(支持动态 schema)
@Configuration
public class DataSourceConfig {
@Bean
@Primary
public DataSource dataSource(@Value("${spring.datasource.url}") String baseUrl,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password) {
// 使用 HikariCP 构建基础数据源(不含 schema)
HikariDataSource baseDs = new HikariDataSource();
baseDs.setJdbcUrl(baseUrl); // e.g., "jdbc:mysql://localhost:3306"
baseDs.setUsername(username);
baseDs.setPassword(password);
// 构建多租户路由数据源
TenantRoutingDataSource routingDs = new TenantRoutingDataSource();
Map<object object> targetDataSources = new HashMap();
// 预注册各租户 schema(实际中可从配置中心/DB 加载)
targetDataSources.put("schema1", createSchemaDataSource(baseDs, "schema1"));
targetDataSources.put("schema2", createSchemaDataSource(baseDs, "schema2"));
targetDataSources.put("schema3", createSchemaDataSource(baseDs, "schema3"));
routingDs.setTargetDataSources(targetDataSources);
routingDs.setDefaultTargetDataSource(baseDs); // fallback
return routingDs;
}
private DataSource createSchemaDataSource(HikariDataSource base, String schema) {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(base.getJdbcUrl() + "/" + schema); // e.g., .../3306/schema1
ds.setUsername(base.getUsername());
ds.setPassword(base.getPassword());
ds.setDriverClassName(base.getDriverClassName());
return ds;
}
}</object>
✅ 步骤四:HTTP 请求拦截,自动设置租户上下文
@Component
public class TenantResolverFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String host = httpRequest.getHeader("Host");
String tenantId = resolveTenantFromHost(host);
TenantContext.setCurrentTenant(tenantId);
try {
chain.doFilter(request, response);
} finally {
TenantContext.clear(); // 必须清理,防止线程复用污染
}
}
private String resolveTenantFromHost(String host) {
if (host == null) return "schema1";
if (host.startsWith("myapp1.")) return "schema1";
if (host.startsWith("myapp2.")) return "schema2";
if (host.startsWith("myapp3.")) return "schema3";
return "schema1";
}
}
⚠️ 注意事项
-
线程安全:
TenantContext必须使用ThreadLocal并在 filterfinally中清除,尤其在 Tomcat 线程池环境下; - 连接池隔离:每个 schema 对应独立数据源,Hikari 连接池自动管理,无需额外干预;
-
JPA 兼容性:Spring Data JPA 完全兼容
AbstractRoutingDataSource,所有@Repository无需修改; - 扩展性建议:生产环境应将租户映射关系外置(如配置中心、数据库表),避免硬编码。
该方案真正实现了「一份 WAR,多租户隔离」,既满足运维简洁性,又保障数据安全性与可维护性。











