登录
首页 >  文章 >  java教程

MyBatis 通过拦截器(Interceptor)可以在 SQL 执行前进行修改或增强,实现多租户的物理隔离 ID 注入。以下是实现步骤和关键代码示例:一、需求背景在多租户系统中,每个租户的数据需要物理隔离,通常做法是为每条 SQL 自动注入一个租户 ID 字段,例如 tenant_id。二、实现思路使用 MyBatis 的 Interceptor 拦截 SQL 语句解析 SQL 语句,判断是否

时间:2026-05-13 13:16:26 221浏览 收藏

本文深入剖析了在多租户系统中通过 MyBatis-Plus 官方推荐的 `TenantLineInnerInterceptor` 实现安全、可靠、生产就绪的物理数据隔离方案,强调摒弃高风险的手动 SQL 解析拦截器,转而正确实现 `TenantLineHandler` 接口——精准返回 Expression 类型的租户 ID、严格匹配字段名、显式排除公共表;同时揭示 INSERT 自动注入 tenant_id 的本质依赖实体类中 `@TableField(fill = FieldFill.INSERT)` 的声明,并一针见血指出真正的难点不在配置本身,而在构建健壮的租户上下文生命周期管理:必须基于 ThreadLocal 封装 `TenantContext`,由 Web 过滤器或拦截器在请求入口解析(优先级:Header > JWT > 子域名)并出口清理,且对异步线程、Dubbo 调用、MQ 消费、定时任务等所有跨边界场景进行手动透传与重置,否则极易引发数据越权或污染——这是一套融合框架能力、代码规范与架构意识的完整多租户落地实践。

怎么利用 MyBatis 的 Interceptor 插件在执行 SQL 前自动注入多租户的物理隔离 ID

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_IDtenantId
  • 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 MVC HandlerInterceptor 提前解析并 set
  • 解析来源优先级建议:Header X-Tenant-ID > JWT payload 中的 tenant_id > 子域名(如 tenant1.example.com)> fallback 到默认租户(需明确日志告警)
  • 每次请求结束必须调用 TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求

真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。

今天关于《MyBatis 通过拦截器(Interceptor)可以在 SQL 执行前进行修改或增强,实现多租户的物理隔离 ID 注入。以下是实现步骤和关键代码示例:一、需求背景在多租户系统中,每个租户的数据需要物理隔离,通常做法是为每条 SQL 自动注入一个租户 ID 字段,例如 tenant_id。二、实现思路使用 MyBatis 的 Interceptor 拦截 SQL 语句解析 SQL 语句,判断是否为插入或更新操作自动添加 tenant_id = ? 到 SQL 中将租户 ID 值传入参数中三、具体实现步骤1. 创建自定义拦截器类 import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.plugin.*; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import java.sql.Connection; import java.util.Properties; @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})}) public class TenantInterceptor implements Interceptor { private String tenantId; // 可以从上下文获取,如 ThreadLocal 或 RequestContext @Override public Object intercept(Invocation invocation) throws Throwable { Statement》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

资料下载
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>