发布于2026-07-09 阅读(0)
扫一扫,手机访问
多租户隔离这个话题,一旦涉及到MyBatis插件拦截,技术选型的第一个分叉路口就来了:到底该拦截哪个组件?不少人的第一反应是去拦截Executor,毕竟它管着数据库操作的入口。但坦白说,这个选择其实是个坑,而且是个不小的坑。
拦截Executor,等到SQL都已经被编译成BoundSql对象,你再想去改WHERE条件,那就有点“事后诸葛亮”的意思了。MyBatis这时候不会重新解析你的SQL结构,硬改BoundSql.getSql()出来的结果,十有八九是语法错误,或者更糟——漏过滤,数据直接暴露。
Executor只看执行,不参与SQL的结构搭建,这是它的职责边界StatementHandler.prepare(),它调用SqlSource.getBoundSql()后,才开始准备PreparedStatementprepare()被调用之前说到这里,答案就很清楚了:拦截StatementHandler才是正解。
最直接的办法?千万别用字符串拼接。比如说,直接在末尾加一句" AND tenant_id = '" + tenantId + "'"——看上去省事,但原始SQL可能根本没有WHERE子句,或者它是个复杂的UNION查询,甚至嵌套了几层子查询。硬拼上去,不出错才怪。
正确的做法是解析SQL的抽象语法树(AST)。用JSqlParser这种工具,先把SELECT、UPDATE、DELETE语句解析出来,定位到主查询的PlainSelect节点,找到它的getWhere()方法。如果原SQL没有WHERE条件,就用AndExpression来新建一个条件;如果已经有WHERE了,就用BinaryExpression把新的租户条件追加进去。
有一点需要特别留意:INSERT语句的租户隔离不太一样。它不需要改写SQL本身——那是MetaObjectHandler自动填充字段值的职责,插件只管查、改、删时的过滤逻辑。
另外,解析SQL前一定要判断表名。系统表比如sys_user、sys_dict这类,你得把它们加进ignoreTables列表里,否则连管理员登录时查用户表都会空手而归,那就尴尬了。
ThreadLocal几乎是唯一靠谱的选择,但前提是你要自己动手封装,别图省事用RequestContextHolder。为什么这么说?RequestContextHolder强依赖Servlet容器,一旦你用了@Async、定时任务或者RabbitMQ消费者,线程上下文就断了,拿到的tenant_id要么是空的,要么是乱掉的。
实际的工程化做法是:在JWT Filter或者网关层解析完token后,立刻调用TenantContext.setTenantId("t001"),把租户ID存进静态的ThreadLocal里。碰到异步场景,比如线程池,就用TaskDecorator手动透传;如果是@RabbitListener,就在方法开头显式调用TenantContext.setTenantId(...)。
还有一个容易被忽略的点:千万别在拦截器里查数据库去取tenant_id。那等于每次请求都额外多一次数据库查询,完全违背了“零侵入”的设计初衷。
TenantLineInnerInterceptor省事吗?确实省事。但它不是黑盒——你仍然需要清楚它底层的约束,否则上线后出问题,排查起来会更痛苦。
StatementHandler,并且用类似JSqlParser的机制做AST改写TenantLineHandler.getTenantId()方法,返回的是一个Expression对象,而不是简单的字符串——这样它才能支持IN或者IS NULL这类复杂条件doTableFilter(String tableName)返回true表示跳过该表,这个判断逻辑一旦写错,比如漏掉sys_role这样的表,权限体系就等于形同虚设INSERT SELECT、REPLACE INTO这类非常规SQL语句,它并不做处理,需要你自己用自定义插件来兜底最后想说一个最容易被忽视的点:租户上下文的生命周期,必须和SQL的执行严格对齐。一次HTTP请求可能横跨多个线程,比如Dubbo的异步回调、CompletableFuture.thenApply这些场景,ThreadLocal不会自动继承。如果不手动做透传,那就等于没有做隔离。

售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8