MyBatis可通过Executor拦截器+ThreadLocal上下文+SQL重写实现多租户隔离:行级隔离在Executor层为SQL自动添加tenant_id条件并注入参数,动态表名在RoutingStatementHandler层替换{tenant}占位符;需用TransmittableThreadLocal防跨线程泄漏,校验tenantId防注入,并避开DDL与缓存污染。

MyBatis 本身不内置多租户支持,但可通过 Executor 拦截器 + ThreadLocal 上下文 + SQL 重写 实现行级隔离与动态表名拼接,关键在于拦截时机选对、租户标识传递可靠、SQL 改写安全可控。
租户上下文统一管理
用 ThreadLocal 存储当前请求的租户 ID(如 tenant_id),配合过滤器或拦截器在请求入口初始化、响应后清理。避免跨线程泄漏,建议搭配 TransmittableThreadLocal 或 Spring 的 RequestContextHolder(Web 环境)。
- 定义 TenantContext 工具类,提供 setTenantId() / getTenantId() / clear()
- 在 WebMvcConfigurer 的拦截器中从请求头(如 X-Tenant-ID)或 JWT payload 提取租户标识
- 异步场景需显式传递(如 CompletableFuture.withExecutor() 包装 + 手动 copy context)
行级数据隔离:通过 Executor 拦截注入 WHERE 条件
实现 org.apache.ibatis.executor.Executor 的拦截器,在 StatementHandler.prepare 前修改 BoundSql,为 SELECT/UPDATE/DELETE 自动追加租户字段过滤条件(如 AND tenant_id = ?),INSERT 则自动填充租户字段值。
- 拦截点推荐用 @Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})) 和同理的 query 方法
- 使用 BoundSql.getSql() 获取原始 SQL,用正则或 SqlSource 解析(更稳妥)识别是否已有 WHERE,再拼接 tenant_id = #{tenantId}(参数化,防注入)
- 向 ParameterObject 注入 tenantId 参数(若原参数是 Map,直接 put;是 POJO,用反射或 MetaObject 设置)
动态表名拼接:在 TableTokenFilter 中替换占位符
针对分库分表或租户独表场景(如 user_tenant_001),在 SQL 解析阶段将 {tenant} 占位符替换为真实表名后缀。不能在 JDBC 层做字符串拼接,必须在 MyBatis 解析 SQL 后、生成 Statement 前完成。
立即学习“Java免费学习笔记(深入)”;
- 拦截点选 @Signature(type = RoutingStatementHandler.class, method = "prepare", args = {Statement.class, Integer.class})
- 通过 ((BaseStatementHandler) invocation.getTarget()).getBoundSql().getSql() 获取 SQL
- 用 Pattern.compile("user_(\{tenant\})").matcher(sql).replaceAll("user_" + tenantId) 替换(注意转义和大小写)
- 确保表名符合数据库命名规范(如加反引号 `user_` + tenantId + "`" 防关键字冲突)
安全与兼容性要点
租户隔离逻辑必须绕过缓存、事务、批量操作等边界情况,且不影响 MyBatis 的一级/二级缓存语义。
- 禁止在拦截器中修改 MappedStatement(不可变),只改 BoundSql 和参数
- 开启 localCacheScope=STATEMENT 避免一级缓存跨租户污染
- 批量操作(foreach)需逐条处理 tenant_id,不能全局替换
- DDL 语句(CREATE、ALTER)不拦截,由运维脚本或 Flyway 统一管理租户表结构


















