MyBatis 插件必须拦截 StatementHandler,因其负责 SQL 拼接、参数绑定与执行准备;Executor 拦截太晚,ParameterHandler 不参与 SQL 结构改写。

MyBatis 插件拦截的是哪个处理器?
必须拦截 StatementHandler,而不是 Executor 或 ParameterHandler。因为 SQL 语句的最终拼接、参数绑定和执行准备都在 StatementHandler 阶段完成;Executor 拦截太晚(已生成完整 SQL),ParameterHandler 只管参数,不参与 SQL 结构改写。
常见错误是误用 @Intercepts 注解指向 Executor.update,结果发现 WHERE 条件根本加不进去——SQL 已经解析完毕,再改 BoundSql 无效。
- 正确目标方法:
StatementHandler.prepare或StatementHandler.query - 需重写
intercept方法,在调用invocation.proceed()前修改MappedStatement的sqlSource - 推荐用
BoundSql+SqlSource替换方式,避免直接字符串拼接引发 SQL 注入或语法错误
如何安全地改写原始 SQL 并注入 tenant_id 条件?
不能简单在 SQL 字符串末尾追加 AND tenant_id = 'xxx'。原始 SQL 可能无 WHERE 子句、可能含子查询、可能带 UNION,硬拼会破坏语法结构,导致 MySQLSyntaxErrorException 或逻辑错误。
正确做法是用 SQL 解析器(如 JSqlParser)解析 AST,定位到主查询的 WHERE 节点,再插入条件表达式。MyBatis-Plus 内部正是这么做的。
- 若自己实现:优先使用
JSqlParser解析SELECT/UPDATE/DELETE语句,调用PlainSelect.getWhere()获取并合并条件 - 注意处理
INSERT场景:插件不负责自动填充字段值,那是MetaObjectHandler的事;拦截器只管“查/改/删”时的过滤 - 忽略表判断必须在解析前做,比如
sys_user是系统表,不应加租户条件,否则管理员登录都失败
tenant_id 从哪来?ThreadLocal 是唯一可靠方式吗?
是的,ThreadLocal 是当前线程内最直接、最可控的传递方式。JWT Filter、LoginController 或网关层解析租户标识后,必须存入 TenantContext.getTenantId() 这类静态方法封装的 ThreadLocal 中。
别用 Spring 的 RequestContextHolder ——它依赖 Servlet 容器,在异步线程(如 @Async、定时任务、MQ 消费)中会丢失上下文,导致 tenant_id 为空或错乱,数据隔离失效。
- 务必在异步场景手动透传:比如用
TaskDecorator包装线程池,或在RabbitListener中显式调用TenantContext.setTenantId(...) - 不要从数据库查租户 ID 再塞进 ThreadLocal:那意味着每次请求都要多一次 DB 查询,违背“零侵入”初衷
- 测试时容易漏掉:单元测试未初始化
TenantContext,导致所有 SQL 都没加条件,测试通过但线上出事
为什么 TenantLineInnerInterceptor 比手写插件更稳?
因为 TenantLineInnerInterceptor 已内置对复杂 SQL 的兼容逻辑:支持 JOIN 表、子查询、UNION ALL、LIMIT、GROUP BY 等场景,并自动跳过 INSERT INTO ... SELECT 这类嵌套查询中的子句。
手写插件若只处理最简 SELECT * FROM t,上线后遇到 SELECT u.name, d.dept_name FROM user u JOIN dept d ON u.dept_id = d.id 就会挂掉——解析失败或条件加错位置。
- 除非有特殊需求(如多租户 + 部门级双维度过滤),否则直接用
TenantLineInnerInterceptor+ 自定义TenantLineHandler - 注意版本:3.5.3+ 才支持
INSERT场景的自动填充(需配合MetaObjectHandler) - 配置顺序很重要:
TenantLineInnerInterceptor必须在PaginationInnerInterceptor之前注册,否则分页 SQL 被先改写,租户条件可能被吞掉
真正难的不是加一行 AND tenant_id = ?,而是确保它出现在每一个该出现的地方、且只出现在该出现的地方——包括嵌套查询、批量更新、甚至 MyBatis 的缓存 key 计算里。稍有疏忽,租户数据就裸奔。

















