乐于分享
好东西不私藏

MyBatis-Plus 插件详解:分页、乐观锁、多租户一次讲清

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 userUPDATE user 这类高危操作

03

SpringBoot 完整配置


插件注册有严格顺序,建议按「SQL 改造 → 业务增强 → 安全兜底」的顺序配置,避免插件冲突、不生效。以下是可直接复制的生产级配置(换为实际项目包名即可):

@Configuration@MapperScan("com.xxx.server.mapper"// 替换为自己项目的mapper包路径public class MybatisPlusPluginConfig {    // 注册MyBatis-Plus插件容器    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor() {        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();        // 1. 多租户插件(优先配置,最先改造SQL)        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(            new TenantLineHandler() {                // 返回当前租户ID(从上下文获取,根据自己项目改造)                @Override                public Expression getTenantId() {                    return new LongValue(TenantContextHolder.getTenantId());                }                // 租户ID对应的数据库字段名                @Override                public String getTenantIdColumn() {                    return "tenant_code"// 替换为自己项目的租户字段                }                // 忽略租户隔离的表(公共表,如字典表、配置表)                @Override                public 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。

核心工作流程

  1. willDoQuery:拦截查询请求,检测参数中是否有 Page 对象,自动生成 COUNT 语句查询总条数,若总条数为 0,直接终止分页查询,提升性能。

  2. beforeQuery:根据数据库方言,自动改写原始 SQL,拼接分页语句(MySQL 加 LIMIT,Oracle 用 ROW_NUMBER())。

高频避坑点:一对多分页 COUNT 不准确

当存在一对多关联查询(如 user 关联 order)时,MyBatis-Plus 自动优化 COUNT 语句会移除 JOIN,导致 COUNT 结果偏小,此时需手动关闭 JOIN 优化:

// 分页查询(一对多场景)Page<UserDTO&gt; 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 {    @Autowired    private DataScopeService dataScopeService;    @Override    public 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,执行改写后的SQL        PluginUtils.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,它让我们摆脱了重复编写分页、租户、权限等通用逻辑的麻烦,实现「一次配置、全局复用」。

核心要点总结:

  1. InnerInterceptor 是插件扩展的入口,所有内置、自定义插件都基于它实现;

  2. 6 大内置插件覆盖分页、多租户、安全等高频场景,开箱即用,降低开发成本;

  3. 自定义插件优先继承 JsqlParserSupport,安全解析 SQL,避免字符串拼接的风险;

  4. 插件注册有顺序,配置不当会导致不生效;

  5. @InterceptorIgnore 注解 + 代码动态控制,可灵活处理特殊接口,兼顾全局统一与个性化需求。

吃透这套插件体系,既能减少重复编码,让业务层更简洁,也能提升系统的安全性和可维护性,真正实现高效开发。

传送门:https://baomidou.com/