必须拦截 statementhandler,因为 executor 拦截太晚、parameterhandler 不处理 sql 结构;statementhandler.prepare() 是 sql 最终定型节点,需在此前用 jsqlparser 解析 ast 并安全注入租户条件,同时严格管控 tenantcontext 线程传递与表过滤逻辑。

必须拦截 StatementHandler,否则 SQL 改写无效——Executor 拦截太晚,ParameterHandler 根本不碰 SQL 结构。
为什么不能拦 Executor?
常见错误是用 @Intercepts 注解指向 Executor.update 或 Executor.query,结果发现 WHERE 条件加不进去。因为此时 MappedStatement 已封装好完整 BoundSql,SQL 字符串早已生成完毕,再改 BoundSql.getSql() 属于“事后补救”,MyBatis 不会重新解析或校验语法,极大概率触发 MySQLSyntaxErrorException 或漏过滤。
-
Executor负责执行,不参与 SQL 拼接与结构构建 -
StatementHandler.prepare()才是 SQL 最终定型的节点:它调用SqlSource.getBoundSql(),并准备PreparedStatement - 所有安全的 SQL 改写(如插入
tenant_id = ?)必须发生在这个方法调用之前
怎么安全地改写 SQL 而不破坏语法?
不能直接字符串拼接 " AND tenant_id = '" + tenantId + "'"——原始 SQL 可能没有 WHERE、可能含 UNION、可能嵌套子查询,硬追加必然出错。
- 正确路径是解析 SQL AST:用
JSqlParser解析SELECT/UPDATE/DELETE语句,定位主查询的PlainSelect.getWhere() - 若原无
WHERE,用AndExpression合并;若有,则用BinaryExpression追加条件 -
INSERT场景无需改写 SQL,那是MetaObjectHandler的事(自动填充字段值),插件只管查/改/删的过滤逻辑 - 务必在解析前判断表名:系统表如
sys_user、sys_dict必须进ignoreTables列表,否则管理员登录时连用户表都查不到
TenantContext 的 tenant_id 从哪来、怎么传才可靠?
ThreadLocal 是唯一可控方式,但必须自己封装,不能依赖 RequestContextHolder。
- JWT Filter 或网关层解析 token 后,立刻调用
TenantContext.setTenantId("t001")存入静态ThreadLocal -
RequestContextHolder依赖 Servlet 容器,在@Async、定时任务、RabbitMQ 消费线程中会丢失上下文,导致tenant_id为空或错乱 - 异步场景必须手动透传:比如用
TaskDecorator包装线程池,或在@RabbitListener方法开头显式调用TenantContext.setTenantId(...) - 千万别在拦截器里查数据库取
tenant_id:那等于每次请求多一次 DB 查询,违背“零侵入”设计初衷
用 MyBatis-Plus 的 TenantLineInnerInterceptor 真的省事吗?
是的,但它不是黑盒——你仍需理解底层约束,否则上线后踩坑更难排查。
- 它内部也是拦
StatementHandler,并用类似JSqlParser的机制做 AST 改写 - 必须实现
TenantLineHandler.getTenantId()返回Expression(不是字符串!),否则无法支持IN或IS NULL等复杂条件 -
doTableFilter(String tableName)返回true表示跳过该表,这个逻辑一旦写错(比如漏了sys_role),权限体系就崩了 - 它不处理
INSERT SELECT、REPLACE INTO等非常规语句,这些得靠自定义插件兜底
最易被忽略的点:租户上下文的生命周期必须和 SQL 执行严格对齐。一次 HTTP 请求可能跨多个线程(如 Dubbo 异步回调、CompletableFuture.thenApply),ThreadLocal 不会自动继承,不手动透传就等于没隔离。










