MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,它在 MyBatis 的基础上只做增强,不做改变,为简化开发、提高效率而生。在实际开发中,我们常常需要对 SQL 执行过程进行拦截和增强,比如分页、SQL 性能分析、SQL 重写、数据权限控制等。InnerInterceptor 是 MyBatis-Plus 提供的一种强大的插件机制,用于在 SQL 执行前后进行拦截和处理。


一、核心概念

1. InnerInterceptor 是什么?

InnerInterceptor 是 MyBatis-Plus 框架中用于拦截 SQL 执行过程的核心接口。它是 MyBatis-Plus 自研的拦截器机制,相比原生 MyBatis 的 InterceptorInnerInterceptor 更加贴近 MP 的执行流程,能够更方便地获取 MP 的上下文信息(如 MetaObjectMappedStatementBoundSql 等)。

InnerInterceptor 接口定义了多个方法,用于在 SQL 执行的不同阶段进行拦截:

  • beforeQuery:查询前拦截
  • beforePrepare:SQL 准备前拦截
  • afterAll:所有操作后拦截
  • beforeUpdate:更新前拦截
  • willDoQuery:即将执行查询时拦截(可用于分页)
  • willDoUpdate:即将执行更新时拦截

2. 与 MyBatis 原生 Interceptor 的区别

特性 MyBatis 原生 Interceptor MyBatis-Plus InnerInterceptor
拦截粒度 拦截 ExecutorStatementHandler 拦截 MP 内部执行流程,更贴近业务
上下文信息 需手动解析 Invocation 提供 PluginChainMetaObject 等封装
易用性 较复杂,需理解 MyBatis 内部机制 更简单,适合 MP 场景
扩展性 通用性强 专为 MP 设计,集成度高

二、操作步骤(非常详细)

下面以实现一个 SQL 打印与执行时间统计InnerInterceptor 插件为例,详细说明开发步骤。

步骤 1:创建 Maven 项目并引入依赖

<dependencies>
    <!-- MyBatis-Plus 核心依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version> <!-- 推荐使用最新稳定版 -->
    </dependency>

    <!-- Spring Boot Web(可选,用于测试) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.12</version>
    </dependency>

    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

步骤 2:编写自定义 InnerInterceptor 实现类

import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.util.Properties;

/**
 * 自定义 InnerInterceptor:SQL 打印与执行时间统计
 */
public class SqlPrintInterceptor implements InnerInterceptor {

    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SqlPrintInterceptor.class);

    /**
     * 查询前拦截:用于记录开始时间
     */
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                            ResultHandler resultHandler, BoundSql boundSql) {
        long startTime = System.currentTimeMillis();
        // 将开始时间存入上下文,便于后续使用
        executor.getTransaction().getConnection().setClientInfo("startTime", String.valueOf(startTime));
    }

    /**
     * 所有操作后拦截:用于计算执行时间并打印 SQL
     */
    @Override
    public void afterAll(Executor executor, MappedStatement ms, Object parameter, BoundSql boundSql) {
        try {
            Connection connection = executor.getTransaction().getConnection();
            String startTimeStr = connection.getClientInfo("startTime");
            if (startTimeStr != null) {
                long startTime = Long.parseLong(startTimeStr);
                long endTime = System.currentTimeMillis();
                long costTime = endTime - startTime;

                // 获取 SQL
                String sql = boundSql.getSql();
                // 获取参数
                Object param = boundSql.getParameterObject();

                log.info("【SQL 执行耗时】: {} ms", costTime);
                log.info("【SQL】: {}", sql);
                log.info("【参数】: {}", param);
            }
        } catch (Exception e) {
            log.error("SQL 日志记录失败", e);
        }
    }

    /**
     * 插件初始化(可选)
     */
    @Override
    public void setProperties(Properties props) {
        // 可以从配置文件读取参数
    }
}

⚠️ 注意:getClientInfo 并非标准 JDBC 方法,此处仅为示例。实际中建议使用 ThreadLocalPluginChain 上下文传递数据。

步骤 3:使用 ThreadLocal 优化上下文传递

public class SqlPrintInterceptor implements InnerInterceptor {

    private static final ThreadLocal<Long> startTimeThreadLocal = new ThreadLocal<>();

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                            ResultHandler resultHandler, BoundSql boundSql) {
        startTimeThreadLocal.set(System.currentTimeMillis());
    }

    @Override
    public void afterAll(Executor executor, MappedStatement ms, Object parameter, BoundSql boundSql) {
        Long startTime = startTimeThreadLocal.get();
        if (startTime != null) {
            long costTime = System.currentTimeMillis() - startTime;
            String sql = boundSql.getSql();
            Object param = boundSql.getParameterObject();

            log.info("【SQL 执行耗时】: {} ms", costTime);
            log.info("【SQL】: {}", sql);
            log.info("【参数】: {}", param);

            // 清理 ThreadLocal
            startTimeThreadLocal.remove();
        }
    }
}

步骤 4:注册插件到 MyBatis-Plus

方式一:Spring Boot 配置类

import com.baomidou.mybatisplus.extension.plugins.MyBatisPlusInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MyBatisPlusInterceptor myBatisPlusInterceptor() {
        MyBatisPlusInterceptor interceptor = new MyBatisPlusInterceptor();
        // 添加自定义插件
        interceptor.addInnerInterceptor(new SqlPrintInterceptor());
        return interceptor;
    }
}

方式二:直接在 application.yml 中配置(不推荐用于自定义逻辑)

仅适用于 MP 内置插件,如分页插件。


三、常见错误

错误 原因 解决方案
插件未生效 未正确注册到 MyBatisPlusInterceptor 检查 @Bean 是否被 Spring 扫描到
ThreadLocal 内存泄漏 未调用 remove() afterAll 中务必 remove()
SQL 打印乱码 参数包含复杂对象 使用 JSON.toJSONString() 格式化参数
多线程下数据错乱 共享变量未隔离 使用 ThreadLocal 隔离线程数据
afterAll 未执行 异常中断 确保异常被捕获,不影响插件执行

四、注意事项

  1. 线程安全InnerInterceptor 是单例的,所有方法可能被多线程并发调用,避免使用实例变量。
  2. 性能影响:插件逻辑应尽量轻量,避免阻塞或复杂计算。
  3. 异常处理:插件内部异常不应影响主流程,建议 try-catch 包裹。
  4. SQL 格式化:生产环境建议格式化 SQL,便于阅读。
  5. 日志级别:建议使用 DEBUGTRACE 级别,避免生产日志过多。

五、使用技巧

  1. 结合 AOP 做更复杂控制:如结合 Spring AOP 实现权限拦截。
  2. 动态开关插件:通过配置中心动态启用/禁用插件。
  3. SQL 重写:在 beforeQuery 中修改 boundSql.sql 实现 SQL 重写(如添加租户字段)。
  4. 慢 SQL 监控:在 afterAll 中判断执行时间,超过阈值则告警。
  5. 结合 SkyWalking / Prometheus:将 SQL 执行时间上报监控系统。

六、最佳实践与性能优化

最佳实践

  • ✅ 使用 ThreadLocal 传递上下文数据
  • ✅ 插件逻辑轻量,避免复杂计算
  • ✅ 日志输出可配置(开关、级别、格式)
  • ✅ 插件可插拔,支持动态加载
  • ✅ 单元测试覆盖核心逻辑

性能优化

  1. 减少日志 I/O:使用异步日志(如 Logback AsyncAppender)
  2. 缓存 SQL 解析结果:对频繁执行的 SQL 可缓存其结构
  3. 避免重复解析BoundSql 已经是解析后的结果,无需再次解析
  4. 使用对象池:如 StringBuilder 复用,减少 GC
  5. 采样日志:高并发场景下可采样打印 SQL(如每 100 次打印一次)

七、扩展:实现分页插件(简化版)

public class SimplePaginationInterceptor implements InnerInterceptor {

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                            ResultHandler resultHandler, BoundSql boundSql) {
        if (rowBounds != RowBounds.DEFAULT) {
            // 重写 SQL 实现分页(此处简化,实际应使用 MP 自带分页)
            String originalSql = boundSql.getSql();
            String paginatedSql = originalSql + " LIMIT " + rowBounds.getOffset() + "," + rowBounds.getLimit();
            // 修改 boundSql(需通过反射或 PluginChain 修改)
        }
    }
}

⚠️ 实际开发中建议使用 MP 内置的 PaginationInnerInterceptor


总结

通过实现 InnerInterceptor,你可以深度定制 MyBatis-Plus 的 SQL 执行流程,实现日志、监控、权限、分页、SQL 重写等高级功能。掌握其核心原理和开发步骤,能够显著提升开发效率和系统可观测性。