MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,它在 MyBatis 的基础上只做增强,不做改变,为简化开发、提高效率而生。在实际开发中,我们常常需要对 SQL 执行过程进行拦截和增强,比如分页、SQL 性能分析、SQL 重写、数据权限控制等。InnerInterceptor
是 MyBatis-Plus 提供的一种强大的插件机制,用于在 SQL 执行前后进行拦截和处理。
一、核心概念
1. InnerInterceptor
是什么?
InnerInterceptor
是 MyBatis-Plus 框架中用于拦截 SQL 执行过程的核心接口。它是 MyBatis-Plus 自研的拦截器机制,相比原生 MyBatis 的 Interceptor
,InnerInterceptor
更加贴近 MP 的执行流程,能够更方便地获取 MP 的上下文信息(如 MetaObject
、MappedStatement
、BoundSql
等)。
InnerInterceptor
接口定义了多个方法,用于在 SQL 执行的不同阶段进行拦截:
beforeQuery
:查询前拦截beforePrepare
:SQL 准备前拦截afterAll
:所有操作后拦截beforeUpdate
:更新前拦截willDoQuery
:即将执行查询时拦截(可用于分页)willDoUpdate
:即将执行更新时拦截
2. 与 MyBatis 原生 Interceptor
的区别
特性 | MyBatis 原生 Interceptor |
MyBatis-Plus InnerInterceptor |
---|---|---|
拦截粒度 | 拦截 Executor 、StatementHandler 等 |
拦截 MP 内部执行流程,更贴近业务 |
上下文信息 | 需手动解析 Invocation |
提供 PluginChain 、MetaObject 等封装 |
易用性 | 较复杂,需理解 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 方法,此处仅为示例。实际中建议使用ThreadLocal
或PluginChain
上下文传递数据。
步骤 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 未执行 |
异常中断 | 确保异常被捕获,不影响插件执行 |
四、注意事项
- 线程安全:
InnerInterceptor
是单例的,所有方法可能被多线程并发调用,避免使用实例变量。 - 性能影响:插件逻辑应尽量轻量,避免阻塞或复杂计算。
- 异常处理:插件内部异常不应影响主流程,建议
try-catch
包裹。 - SQL 格式化:生产环境建议格式化 SQL,便于阅读。
- 日志级别:建议使用
DEBUG
或TRACE
级别,避免生产日志过多。
五、使用技巧
- 结合 AOP 做更复杂控制:如结合 Spring AOP 实现权限拦截。
- 动态开关插件:通过配置中心动态启用/禁用插件。
- SQL 重写:在
beforeQuery
中修改boundSql.sql
实现 SQL 重写(如添加租户字段)。 - 慢 SQL 监控:在
afterAll
中判断执行时间,超过阈值则告警。 - 结合 SkyWalking / Prometheus:将 SQL 执行时间上报监控系统。
六、最佳实践与性能优化
最佳实践
- ✅ 使用
ThreadLocal
传递上下文数据 - ✅ 插件逻辑轻量,避免复杂计算
- ✅ 日志输出可配置(开关、级别、格式)
- ✅ 插件可插拔,支持动态加载
- ✅ 单元测试覆盖核心逻辑
性能优化
- 减少日志 I/O:使用异步日志(如 Logback AsyncAppender)
- 缓存 SQL 解析结果:对频繁执行的 SQL 可缓存其结构
- 避免重复解析:
BoundSql
已经是解析后的结果,无需再次解析 - 使用对象池:如
StringBuilder
复用,减少 GC - 采样日志:高并发场景下可采样打印 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 重写等高级功能。掌握其核心原理和开发步骤,能够显著提升开发效率和系统可观测性。