MyBatis Interceptor 通过拦截 StatementHandler 实现分页(重写 SQL 并注入参数)、拦截 Executor 实现审计(统计耗时、记录 SQL 与参数),需注意拦截顺序、线程安全及上下文获取。

MyBatis 的 Interceptor 接口是其核心插件机制,允许你在 SQL 执行的关键节点(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)前后插入自定义逻辑。实现分页或 SQL 审计,本质上就是拦截 StatementHandler(修改 SQL)或 Executor(增强执行行为),再结合责任链完成。
分页插件:重写 SQL + 拦截查询流程
主流分页方案(如 PageHelper)正是基于 StatementHandler 拦截实现的。核心思路是:在查询语句执行前,解析原始 SQL,自动拼接 LIMIT ?, ?(MySQL)或 ROWNUM(Oracle)等方言分页子句,并将分页参数注入到 ParameterHandler 中。
-
拦截目标:通常指定
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})) -
获取原始 SQL:通过
MetaObject反射拿到BoundSql中的sql字段,注意要先解包RoutingStatementHandler → BaseStatementHandler -
重写 SQL:用正则或 SQL 解析器(如 JSQLParser)识别
SELECT,避免误改INSERT/UPDATE;对带ORDER BY、子查询、UNION的语句需特殊处理 -
设置参数:调用
boundSql.setAdditionalParameter("pageOffset", offset)并确保后续能被 JDBC 正确绑定 - 注意事务与缓存:分页不应影响二级缓存 key,PageHelper 默认将分页参数从 cacheKey 中排除
SQL 审计插件:记录执行耗时、参数与结果行数
审计插件更适合拦截 Executor 的 query 和 update 方法,在执行前后采集上下文信息,不修改 SQL 本身。
-
拦截点选择:推荐
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) -
统计执行时间:在
invocation.proceed()前后记录System.nanoTime(),计算耗时(纳秒转毫秒) -
获取完整 SQL:调用
ms.getBoundSql(parameter).getSql(),再通过DefaultParameterHandler渲染参数值(注意脱敏敏感字段如 password、id_card) -
捕获返回结果:query 返回
List,可记录 size;update 返回影响行数,直接取返回值即可 - 日志输出建议:使用异步日志(如 Logback 的 AsyncAppender)避免阻塞主线程,按 traceId 关联上下游调用
注册插件:配置与注意事项
插件需在 MyBatis 配置中声明,Spring Boot 项目通常通过 @Bean 注入 SqlSessionFactory 的 plugins 属性。
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- 顺序很重要:多个插件按注册顺序组成责任链;分页插件应早于审计插件,否则审计看到的是已改写的 SQL
-
避免重复拦截:用
MappedStatement.getSqlCommandType()过滤非查询语句,防止 update 被错误分页 -
线程安全:Interceptor 实例是单例,不要在类中保存 request 级别状态;如需上下文,可用
ThreadLocal(记得 remove 防内存泄漏) -
兼容性考虑:MyBatis 3.4+ 支持
plugin方法自动代理,无需手动调用Plugin.wrap();低版本需显式包装
简单示例:一个轻量审计拦截器
以下是一个打印 SQL、参数和耗时的最小可行插件:
Java 类中:
public class SqlAuditInterceptor implements Interceptor {private Logger log = LoggerFactory.getLogger(getClass());
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.nanoTime();
Object result = invocation.proceed();
long cost = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object param = args[1];
BoundSql boundSql = ms.getBoundSql(param);
String sql = boundSql.getSql().replaceAll("[\s]+", " ").trim();
log.debug("SQL: {} | Params: {} | Cost: {}ms", sql, param, cost);
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
不复杂但容易忽略细节,关键在选对拦截点、拿准上下文、做好异常兜底。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










