TenantLineInnerInterceptor 是 MyBatis-Plus 唯一推荐的多租户实现方式,需正确实现 TenantLineHandler:getTenantId() 返回 Expression 并处理 null,getTenantIdColumn() 严格匹配字段名,ignoreTable 显式排除公共表;INSERT 自动注入依赖实体类中 @TableField(fill = FieldFill.INSERT) 声明 tenant_id 字段;租户 ID 必须通过 ThreadLocal 绑定请求生命周期,由 Web 过滤器或拦截器解析并清理,跨线程/远程调用需手动透传。

MyBatis-Plus 的 TenantLineInnerInterceptor 是唯一推荐路径
直接用 MyBatis 原生 Interceptor 手动解析 SQL 并注入租户条件,风险极高、维护成本爆炸。MyBatis-Plus 内置的 TenantLineInnerInterceptor 已覆盖所有标准 DML 场景(SELECT/INSERT/UPDATE/DELETE),且经过大量 SaaS 生产环境验证。别自己造轮子。
TenantLineInnerInterceptor 怎么正确配置 tenant_id 注入逻辑
核心是实现 TenantLineHandler 接口,重点不在“怎么写”,而在“怎么取”和“怎么防错”:
-
getTenantId()必须返回Expression类型,不能直接 return 字符串或数字;常见错误是返回new StringValue("xxx")却没处理 null 场景——一旦TenantContext.getCurrentTenantId()为 null,整个 SQL 会崩掉 -
getTenantIdColumn()值必须与数据库表中真实字段名完全一致(大小写敏感),比如 MySQL 表定义是tenant_id,就不能写成TENANT_ID或tenantId -
ignoreTable(String tableName)要显式排除公共表,例如"sys_dict"、"sys_config";漏掉会导致这些表被错误加上WHERE tenant_id = ?,查不到全局数据
为什么 INSERT 语句能自动补 tenant_id 字段
插件不是简单拼字符串,而是用 JSQLParser 解析 AST 后,在 INSERT 的 column list 和 values list 中双向注入:
INSERT INTO user (name, email) VALUES (?, ?) → INSERT INTO user (name, email, tenant_id) VALUES (?, ?, ?)
但前提是你的实体类(如 User)里必须声明 tenant_id 字段,并加 @TableField(fill = FieldFill.INSERT) 注解,否则 MP 不知道该往哪填值。漏掉这个注解,INSERT 成功但数据无租户归属,等于裸奔。
租户 ID 来源必须绑定请求生命周期,不能靠静态变量
最常踩的坑:在 getTenantId() 里直接调用 SecurityContextHolder.getContext().getAuthentication() 或硬编码,导致异步线程、定时任务、单元测试中取不到值,或跨请求污染。
- 务必用
ThreadLocal封装TenantContext,并在 Web 层用OncePerRequestFilter或 Spring MVCHandlerInterceptor提前解析并 set - 解析来源优先级建议:Header
X-Tenant-ID> JWT payload 中的tenant_id> 子域名(如tenant1.example.com)> fallback 到默认租户(需明确日志告警) - 每次请求结束必须调用
TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求
真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。

















