【Mybatis-Plus源码探秘】多租户插件核心拦截机制深度解析与实战配置

📅 2026/6/28 18:39:06
【Mybatis-Plus源码探秘】多租户插件核心拦截机制深度解析与实战配置
1. 多租户插件的前世今生第一次接触多租户概念是在2015年做SaaS平台时当时为了给不同客户隔离数据硬是在每个SQL后面手动拼接AND tenant_idxxx。这种土办法不仅容易出错还经常忘记加条件。直到发现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时拦截器的工作流程堪比精密仪器拦截触发MybatisPlusInterceptor拦截所有SQL请求租户过滤TenantLineInnerInterceptor的beforeQuery方法接管处理语法解析使用JSqlParser将SQL解析为语法树条件注入在WHERE子句中插入tenant_id 1条件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.idb.aid为例检查表a是否需要租户过滤检查表b是否需要租户过滤对需要过滤的表分别添加条件最终生成select * from a join b on a.idb.aid WHERE a.tenant_id1 AND b.tenant_id1这里有个性能优化点如果多表查询的所有表都属于同一租户可以考虑在应用层先校验租户一致性减少数据库过滤开销。5. 生产环境避坑指南5.1 与分页插件的相爱相杀在同时使用多租户和分页插件时我遇到过count查询漏加租户条件的问题。解决方案是确保拦截器添加顺序正确并且自定义count查询select idselectPageCount resultTypelong SELECT COUNT(*) FROM table WHERE tenant_id #{tenantId} AND other_conditions /select5.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. 源码调试技巧要深入理解插件工作原理推荐按这个顺序调试在MybatisPlusInterceptor.intercept方法打断点观察interceptors集合中拦截器的顺序进入TenantLineInnerInterceptor.beforeQuery跟踪JSqlParser解析过程观察最终生成的SQL调试时会发现插件对Batch操作、存储过程等特殊场景都有处理逻辑。比如批量插入时会自动为每条记录设置tenant_id值。在RuoYi-Vue-Plus框架中集成时特别注意要排除系统内置表的租户过滤。框架默认的ignoreTable实现通常已经包含这些处理但二次开发时可能需要调整。