【Mybatis-Plus源码探秘】多租户插件核心拦截机制深度解析与实战配置
2026/8/3 5:52:44 网站建设 项目流程

1. 多租户插件的前世今生

第一次接触多租户概念是在2015年做SaaS平台时,当时为了给不同客户隔离数据,硬是在每个SQL后面手动拼接"AND tenant_id=xxx"。这种土办法不仅容易出错,还经常忘记加条件。直到发现Mybatis-Plus的多租户插件,才明白原来数据隔离可以如此优雅。

多租户(Multi-Tenancy)的本质就像一栋写字楼:整栋楼共用基础设施(数据库实例),但每个公司(租户)拥有独立的办公区域(数据空间)。在技术实现上,主要分为三种模式:

  • 独立数据库:成本最高但隔离性最好
  • 共享数据库独立Schema:折中方案
  • 共享数据库共享Schema:成本最低但需要字段隔离

Mybatis-Plus选择的是第三种方案,通过tenant_id字段实现数据隔离。这就像给每张桌子(数据表)贴上公司标签(tenant_id),查询时自动过滤非本公司物品。

2. 核心拦截器工作原理揭秘

2.1 拦截器链的装配过程

在SpringBoot项目中配置多租户插件时,我们需要在MybatisPlusInterceptor中添加TenantLineInnerInterceptor。这个顺序很重要——就像机场安检,必须先在登机口(分页拦截器)前完成身份核验(租户过滤)。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 必须先添加租户拦截器 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler())); // 再添加分页拦截器 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; }

2.2 SQL拦截的完整流程

当执行select * from sys_user时,拦截器的工作流程堪比精密仪器:

  1. 拦截触发:MybatisPlusInterceptor拦截所有SQL请求
  2. 租户过滤:TenantLineInnerInterceptor的beforeQuery方法接管处理
  3. 语法解析:使用JSqlParser将SQL解析为语法树
  4. 条件注入:在WHERE子句中插入tenant_id = 1条件
  5. SQL重构:将修改后的语法树重新生成SQL语句

这个过程中最精妙的是JSqlParser的运用。它就像SQL翻译官,把字符串SQL转换成可操作的Java对象树,让我们能精准修改查询条件。

3. 实战中的关键配置技巧

3.1 租户处理器的定制开发

TenantLineHandler是插件的大脑,需要实现三个核心方法:

public TenantLineHandler tenantLineHandler() { return new TenantLineHandler() { // 获取当前租户ID(从ThreadLocal或SecurityContext) @Override public Expression getTenantId() { return new LongValue(SecurityUtils.getTenantId()); } // 指定租户字段名 @Override public String getTenantIdColumn() { return "tenant_id"; } // 忽略特定表(字典表等公共数据) @Override public boolean ignoreTable(String tableName) { return Arrays.asList("sys_dict", "sys_config").contains(tableName); } }; }

实际项目中我踩过的坑:getTenantId()方法不能返回null,否则会导致NPE。建议像上面代码那样设置默认租户ID。

3.2 忽略表的智能判断

ignoreTable方法的实现往往需要结合业务场景。在电商系统中,商品表可能需要区分平台商品(忽略租户)和商家商品(需要租户隔离)。我常用的模式是:

@Override public boolean ignoreTable(String tableName) { // 公共表直接忽略 if(publicTables.contains(tableName)) return true; // 超级管理员跳过过滤 if(SecurityUtils.isSuperAdmin()) return true; // 特定业务场景判断 if("order".equals(tableName) && isCrossTenantQuery()){ return true; } return false; }

4. 深度源码解析

4.1 条件构造的玄机

在TenantLineInnerInterceptor.builderExpression方法中,可以看到条件拼接的核心逻辑:

protected Expression builderExpression(...) { // 基础条件:tenant_id = 1 EqualsTo equalsTo = new EqualsTo(); equalsTo.setLeftExpression(new Column(tenantLineHandler.getTenantIdColumn())); equalsTo.setRightExpression(tenantLineHandler.getTenantId()); // 已有WHERE条件时用AND连接 if(existingWhere != null) { return new AndExpression(existingWhere, equalsTo); } return equalsTo; }

这个设计体现了Mybatis-Plus的巧妙之处:不是简单拼接字符串,而是在语法树层面进行操作,避免了SQL注入风险。

4.2 多表查询的特殊处理

在处理JOIN查询时,插件会递归处理所有表引用。以select * from a join b on a.id=b.aid为例:

  1. 检查表a是否需要租户过滤
  2. 检查表b是否需要租户过滤
  3. 对需要过滤的表分别添加条件
  4. 最终生成select * from a join b on a.id=b.aid WHERE a.tenant_id=1 AND b.tenant_id=1

这里有个性能优化点:如果多表查询的所有表都属于同一租户,可以考虑在应用层先校验租户一致性,减少数据库过滤开销。

5. 生产环境避坑指南

5.1 与分页插件的相爱相杀

在同时使用多租户和分页插件时,我遇到过count查询漏加租户条件的问题。解决方案是确保拦截器添加顺序正确,并且自定义count查询:

<select id="selectPageCount" resultType="long"> SELECT COUNT(*) FROM table WHERE tenant_id = #{tenantId} AND other_conditions </select>

5.2 事务传播的特殊情况

在@Transactional方法中切换租户上下文时,新租户ID可能不会立即生效。这是因为拦截器在事务开始时就已经确定SQL模板。解决方法是在事务方法内显式清除Mybatis缓存:

@Transactional public void crossTenantOperation() { // 操作租户A数据 mapperA.doSomething(); // 清除缓存使新租户ID生效 SqlSessionHelper.clearCache(sqlSessionFactory); // 操作租户B数据 mapperB.doSomething(); }

5.3 性能监控建议

在多租户系统中,建议对SQL执行进行监控,特别关注:

  • 漏加租户条件的SQL(安全风险)
  • 全表扫描的查询(性能风险)
  • 跨租户的大结果集查询(内存风险)

可以在TenantLineHandler中添加统计逻辑:

@Override public Expression getTenantId() { String tenantId = SecurityUtils.getTenantId(); Metrics.counter("tenant.query", "tenantId", tenantId).increment(); return new StringValue(tenantId); }

6. 扩展应用场景

6.1 多租户+数据权限组合

结合数据权限插件可以实现更细粒度的控制。比如部门经理只能查看本部门数据,而租户管理员可以查看整个租户数据。配置示例:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 数据权限拦截器 interceptor.addInnerInterceptor(new DataPermissionInterceptor( dataPermissionHandler())); // 多租户拦截器 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor( tenantLineHandler())); return interceptor; }

6.2 动态租户字段

有些业务需要同时按organization_id和tenant_id过滤。可以通过继承TenantLineHandler实现:

@Override public String getTenantIdColumn() { if(isOrganizationQuery()){ return "organization_id"; } return "tenant_id"; }

7. 源码调试技巧

要深入理解插件工作原理,推荐按这个顺序调试:

  1. 在MybatisPlusInterceptor.intercept方法打断点
  2. 观察interceptors集合中拦截器的顺序
  3. 进入TenantLineInnerInterceptor.beforeQuery
  4. 跟踪JSqlParser解析过程
  5. 观察最终生成的SQL

调试时会发现,插件对Batch操作、存储过程等特殊场景都有处理逻辑。比如批量插入时,会自动为每条记录设置tenant_id值。

在RuoYi-Vue-Plus框架中集成时,特别注意要排除系统内置表的租户过滤。框架默认的ignoreTable实现通常已经包含这些处理,但二次开发时可能需要调整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询