MyBatis-Plus 插件详解:分页、乐观锁、多租户一次讲清
“ 作为 Java 后端开发者,MyBatis-Plus 几乎已经成为 SpringBoot 项目的标配。
大多数人日常只用到基础 CRUD 和条件构造器,却很少真正深入它的插件体系——这个能让开发效率翻倍、系统更安全的核心能力,被太多人忽略了。
今天这篇文章,就带你从原理到实战,完整吃透 MyBatis-Plus 插件机制,让分页、多租户、数据权限、防 SQL 攻击等通用能力,真正做到一次配置、全局生效,摆脱重复编码的内耗。”

01
—
InnerInterceptor 是什么?
从 MyBatis-Plus 3.4.0 版本开始,官方重构了插件体系,将所有扩展能力统一收口到一个核心接口——InnerInterceptor。
通俗来讲,它就是 MyBatis-Plus 的SQL 拦截与增强中枢:不用改动任何业务代码,就能在 SQL 执行的各个阶段(准备、查询、更新前)插入自定义逻辑,实现 SQL 改写、权限控制、性能监控等功能。
核心接口方法(简化版,保留常用核心):
public interface InnerInterceptor {// 判断是否执行查询操作,返回false则终止查询default boolean willDoQuery(Executor executor, MappedStatement ms,Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) {return true;}// 查询执行前的核心拦截方法(最常用,可改写SQL)default void beforeQuery(Executor executor, MappedStatement ms,Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) {}// 判断是否执行更新操作(新增/修改/删除)default boolean willDoUpdate(Executor executor, MappedStatement ms,Object parameter, BoundSql boundSql) {return true;}// 更新执行前拦截,可做数据校验、SQL改写default void beforeUpdate(Executor executor, MappedStatement ms,Object parameter, BoundSql boundSql) {}}记住核心逻辑:实现该接口 → 注册到 MybatisPlusInterceptor 容器 → 拦截逻辑自动生效,全程无侵入、不污染业务代码。
02
—
MyBatis-Plus 内置六大生产级插件
MyBatis-Plus 早已为我们封装好 6 个高频生产级插件,覆盖日常开发 90% 场景,不用手写一行额外代码,配置即生效。这些插件,本质上都是 InnerInterceptor 的具体实现类。
-
PaginationInnerInterceptor:自动物理分页,支持 MySQL、Oracle 等多数据库方言,无需手动拼接 LIMIT 语句
-
TenantLineInnerInterceptor:多租户行级隔离,自动给 SQL 追加租户 ID 条件,适配 SaaS 系统
-
DynamicTableNameInnerInterceptor:动态表名映射,支持分库分表场景下,运行时切换目标表名
-
OptimisticLockerInnerInterceptor:乐观锁实现,通过版本号字段,防止并发更新导致的数据覆盖
-
IllegalSQLInnerInterceptor:SQL 性能与规范校验,禁止全表扫描、强制走索引,规避性能隐患
-
BlockAttackInnerInterceptor:防全表更新/删除,杜绝
DELETE FROM user、UPDATE user这类高危操作
03
—
SpringBoot 完整配置
插件注册有严格顺序,建议按「SQL 改造 → 业务增强 → 安全兜底」的顺序配置,避免插件冲突、不生效。以下是可直接复制的生产级配置(换为实际项目包名即可):
@Configuration@MapperScan("com.xxx.server.mapper") // 替换为自己项目的mapper包路径public class MybatisPlusPluginConfig {// 注册MyBatis-Plus插件容器@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 1. 多租户插件(优先配置,最先改造SQL)interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {// 返回当前租户ID(从上下文获取,根据自己项目改造)@Overridepublic Expression getTenantId() {return new LongValue(TenantContextHolder.getTenantId());}// 租户ID对应的数据库字段名@Overridepublic String getTenantIdColumn() {return "tenant_code"; // 替换为自己项目的租户字段}// 忽略租户隔离的表(公共表,如字典表、配置表)@Overridepublic boolean ignoreTable(String tableName) {List<String> ignoreTables = Arrays.asList("sys_dict", "sys_config", "sys_menu");return ignoreTables.contains(tableName);}}));// 2. 分页插件(指定数据库类型,避免查询元数据,提升性能)interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));// 3. 乐观锁插件(支持整数、LocalDateTime类型的版本号)interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());// 4. 防全表更新/删除插件(安全兜底,最后配置)interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());return interceptor;}}插件注册顺序总结:多租户/动态表名 → 分页/乐观锁 → SQL 规范/防全表攻击;原则是「对 SQL 做改造的优先,做安全校验的最后」。
04
—
分页插件详解与避坑
在所有插件中,
PaginationInnerInterceptor使用率最高,它的核心作用是「自动处理分页逻辑」,无需手动写 COUNT 查询和分页 SQL。核心工作流程
willDoQuery:拦截查询请求,检测参数中是否有 Page 对象,自动生成 COUNT 语句查询总条数,若总条数为 0,直接终止分页查询,提升性能。
beforeQuery:根据数据库方言,自动改写原始 SQL,拼接分页语句(MySQL 加 LIMIT,Oracle 用 ROW_NUMBER())。
高频避坑点:一对多分页 COUNT 不准确
当存在一对多关联查询(如 user 关联 order)时,MyBatis-Plus 自动优化 COUNT 语句会移除 JOIN,导致 COUNT 结果偏小,此时需手动关闭 JOIN 优化:
// 分页查询(一对多场景)Page<UserDTO> page = new Page<>(1, 10);// 关闭COUNT语句的JOIN优化,避免结果不准确page.setOptimizeJoinOfCountSql(false);// 执行分页查询IPage<UserDTO> userPage = userMapper.selectUserWithOrder(page, queryWrapper);05
—
自定义插件:实现数据权限拦截
当内置插件无法满足业务需求(如数据权限控制、字段脱敏)时,可自定义拦截器。重点提醒:禁止直接字符串拼接 SQL,容易破坏语法、引发 SQL 注入,推荐继承
JsqlParserSupport安全解析 SQL。实战案例:自定义数据权限拦截器(根据登录用户所属部门,过滤数据)
/*** 自定义数据权限拦截器(基于JsqlParser,安全改写SQL)*/public class DataScopeInterceptor extends JsqlParserSupport implements InnerInterceptor {@Autowiredprivate DataScopeService dataScopeService;@Overridepublic void beforeQuery(Executor executor, MappedStatement ms,Object parameter, RowBounds rowBounds,ResultHandler resultHandler, BoundSql boundSql) {// 1. 获取当前登录用户信息(从Security上下文获取,根据项目改造)LoginUser loginUser = SecurityUtils.getLoginUser();if (loginUser == null) {return;}// 2. 获取用户的数据权限范围(所属部门ID集合)List<Long> deptIds = dataScopeService.getDataScopeDeptIds(loginUser.getId());if (CollUtil.isEmpty(deptIds)) {// 无权限,返回空结果(可根据业务调整)throw new BusinessException("无数据访问权限");}// 3. 安全解析并改写SQL,追加数据权限条件String originalSql = boundSql.getSql();String newSql = parserSingle(originalSql, sql -> {// 拼接数据权限条件:dept_id IN (1,2,3)String dataScopeCondition = "dept_id IN (" + CollUtil.join(deptIds, ",") + ")";// 安全合并WHERE条件(避免破坏原有WHERE语法)return this.addWhere(sql, dataScopeCondition, true);});// 4. 替换原始SQL,执行改写后的SQLPluginUtils.mpBoundSql(boundSql).sql(newSql);}}自定义插件注册:在 MybatisPlusPluginConfig 中,添加该插件(按顺序插入对应位置即可)。
06
—
忽略拦截:灵活处理特殊接口
部分接口(如全局统计、系统管理接口)不需要插件拦截,可使用
@InterceptorIgnore注解精准跳过,无需修改插件配置。/*** 忽略多租户插件拦截(查询所有用户,不受租户限制)*/@InterceptorIgnore(tenantLine = "true")@Select("SELECT id, username, create_time FROM sys_user")List<SysUser> selectAllUser();}注解参数对照表(常用)
-
tenantLine = "true":跳过多租户插件 -
dynamicTableName = "true":跳过动态表名插件 -
blockAttack = "true":允许全表更新/删除(谨慎使用) -
illegalSql = "true":跳过 SQL 规范校验
进阶:代码动态忽略(3.5.3+ 版本支持)
当注解无法满足动态场景(如根据用户角色判断是否忽略),可通过代码手动控制,需注意 try-finally 清理,避免影响后续请求:
try {// 手动设置忽略多租户插件InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());// 执行不受租户限制的查询List<SysDict> dictList = dictMapper.selectList(null);} finally {// 必须清理忽略策略,避免影响后续请求InterceptorIgnoreHelper.clearIgnoreStrategy();}07
—
总结
MyBatis-Plus 插件体系的核心是 InnerInterceptor,它让我们摆脱了重复编写分页、租户、权限等通用逻辑的麻烦,实现「一次配置、全局复用」。
核心要点总结:
InnerInterceptor 是插件扩展的入口,所有内置、自定义插件都基于它实现;
6 大内置插件覆盖分页、多租户、安全等高频场景,开箱即用,降低开发成本;
自定义插件优先继承 JsqlParserSupport,安全解析 SQL,避免字符串拼接的风险;
插件注册有顺序,配置不当会导致不生效;
@InterceptorIgnore 注解 + 代码动态控制,可灵活处理特殊接口,兼顾全局统一与个性化需求。
吃透这套插件体系,既能减少重复编码,让业务层更简洁,也能提升系统的安全性和可维护性,真正实现高效开发。
传送门:https://baomidou.com/
夜雨聆风