Java多租户隔离核心是让tenant_id成为隐形默认参数,通过ScopedValue(Java21+)或ThreadLocal统一绑定上下文,DAO层强制注入租户条件,跨线程/服务需透传tenantId,并统一日志与异常的租户归因。

Java 封装多租户数据隔离,核心不是把租户 ID 塞进某个包装类,而是让 tenant_id 成为业务逻辑的“隐形默认参数”——自动携带、自动生效、难以绕过。重点不在封装形式,而在传递可靠性与执行强制性。
用 ScopedValue 或 ThreadLocal 管理租户上下文
避免每次手动传 tenantId,统一在线程(或作用域)级绑定:
- Java 21+ 推荐用 ScopedValue:声明
private static final ScopedValue<String> TENANT_ID = ScopedValue.newInstance();,入口处(如 Filter)调用ScopedValue.where(TENANT_ID, id).run(() -> doFilter(...)) - 兼容老版本可用 ThreadLocal<String>,但需配合 Spring 的
RequestContextFilter或自定义过滤器初始化,并在 finally 块中remove()防泄漏 - 业务层直接
TENANT_ID.get()或TenantContext.getCurrentTenant()获取,DAO、Service、Validator 全局可读
在数据访问层强制注入租户条件
不让原始 SQL 或裸查询暴露风险,所有数据操作必须带租户约束:
- MyBatis:用
<sql>定义通用租户过滤片段,所有 mapper 引用<include refid="tenantFilter"/>;或使用 MyBatis-Plus 的TenantLineInnerInterceptor自动拼接AND tenant_id = ? - JPA:实体继承
TenantEntity,含@Column(name = "tenant_id", insertable = false, updatable = false);配合@PrePersist和@PreUpdate自动填充 - 禁止开放
jdbcTemplate.query(String sql, ...)这类裸接口;只提供queryForTenant(String table, Object params)封装方法
透传租户上下文到跨服务与异步场景
单线程内上下文可靠,但远程调用、消息、定时任务容易断链,需主动透传:
立即学习“Java免费学习笔记(深入)”;
- HTTP 调用:Feign 或 RestTemplate 拦截器自动添加
X-Tenant-IDheader - 消息队列(RabbitMQ/Kafka):发送前将当前
tenantId注入 message headers,消费端从 headers 解析并重新绑定到 ScopedValue/ThreadLocal - 异步任务:使用
TransmittableThreadLocal(TTL)或 SpringTaskDecorator,确保线程池任务继承父线程租户值 - 定时任务:不直接用
@Scheduled,改由租户调度中心注册带tenantId的JobDetail
统一日志与异常的租户归因
让问题可定位、日志可追溯,无需人工打标:
- 日志框架(如 Logback)配合 MDC,在过滤器中
MDC.put("tenantId", tenantId),Pattern 中加入%X{tenantId} - DAO 层捕获数据库异常后,包装为
TenantDatabaseException(tenantId, tableName, e),保留租户上下文 - 全局异常处理器提取
TenantDatabaseException中的 tenantId,记录到错误日志和监控指标中


















