MyBatis 插件机制(拦截器)?

2025年 阅读约 12 分钟 面试指南 · MyBatis

深入解析MyBatis插件机制:四大可拦截对象、Interceptor接口与@Intercepts注解、责任链模式实现、PageHelper分页插件原理、自定义插件实战,附面试模拟问答。

一句话总结

MyBatis 插件机制基于 责任链模式 + JDK 动态代理,可拦截四大核心对象:Executor(SQL 执行)、StatementHandler(语句处理)、ParameterHandler(参数设置)、ResultSetHandler(结果处理)。每个插件实现 Interceptor 接口,通过 @Intercepts + @Signature 注解指定拦截目标,最终通过 InterceptorChain.pluginAll() 层层代理包裹目标对象。著名应用:PageHelper 分页插件拦截 Executor.query() 自动追加 LIMIT。

初级理解

四大可拦截对象

拦截对象可拦截方法作用
Executorupdate、query、flushStatements、commit、rollbackSQL 执行全过程
StatementHandlerprepare、parameterize、batch、update、querySQL 语句预处理
ParameterHandlergetParameterObject、setParameters参数设置
ResultSetHandlerhandleResultSets、handleOutputParameters结果集映射

插件使用步骤

# 1. 实现 Interceptor 接口 @Intercepts({ @Signature( type = Executor.class, // 拦截 Executor method = "query", // 拦截 query 方法 args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) public class MyPlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 前置增强 System.out.println("before query"); Object result = invocation.proceed(); // 执行原方法 // 后置增强 System.out.println("after query"); return result; } } # 2. 注册插件(mybatis-config.xml) <plugins> <plugin interceptor="com.example.plugin.MyPlugin"/> </plugins> # 3. Spring Boot 注册方式 @Configuration public class MyBatisConfig { @Bean public MyPlugin myPlugin() { return new MyPlugin(); } }

中级深入

责任链模式实现

# InterceptorChain 插件链核心代码 public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } // 对所有拦截器依次代理 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } } # Interceptor.plugin() 默认实现 default Object plugin(Object target) { return Plugin.wrap(target, this); } # Plugin.wrap() 核心逻辑 public static Object wrap(Object target, Interceptor interceptor) { // 1. 获取拦截器的 @Signature 注解信息 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); // 2. 获取 target 实现的所有接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 3. 如果有匹配的接口,创建 JDK 动态代理 if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } // 4. 没有匹配,返回原对象 return target; } # 多层代理效果: # Executor → Plugin1代理 → Plugin2代理 → Plugin3代理 # 调用 executor.query() 时: # Plugin3.invoke() → 执行拦截逻辑 → Plugin2.invoke() # → 执行拦截逻辑 → Plugin1.invoke() → 执行拦截逻辑 # → executor.query()(真正执行)

PageHelper 分页插件原理

# PageHelper 拦截 Executor.query() @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) public class PageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 1. 获取参数中的分页信息 Page page = PageHelper.getLocalPage(); if (page == null) return invocation.proceed(); // 2. 查询总记录数 Long count = executeCount(invocation); page.setTotal(count); // 3. 修改原 SQL,追加 LIMIT 子句 String pageSql = getPageSql(boundSql, page); // 4. 执行分页查询 return executePageQuery(invocation, pageSql); } } # 使用方式: PageHelper.startPage(1, 10); // 第1页,每页10条 List<User> users = userMapper.selectAll(); // 自动分页 PageInfo<User> pageInfo = new PageInfo<>(users); # PageHelper 核心:ThreadLocal 传递分页参数 # PageHelper.startPage() → 设置 ThreadLocal # Executor.query() 被拦截 → 读取 ThreadLocal → 修改 SQL # 方法结束后 → 清除 ThreadLocal(防止内存泄漏)

高级进阶

多个插件执行顺序

# 插件执行顺序取决于注册顺序 # XML 配置中先注册的先执行(最外层代理) <plugins> <plugin interceptor="com.example.PluginA"/> <!-- 最外层 --> <plugin interceptor="com.example.PluginB"/> <!-- 中间层 --> <plugin interceptor="com.example.PluginC"/> <!-- 最内层 --> </plugins> # 执行顺序(类似洋葱模型): # PluginA.invoke() 前置 # → PluginB.invoke() 前置 # → PluginC.invoke() 前置 # → 真实方法执行 # → PluginC.invoke() 后置 # → PluginB.invoke() 后置 # → PluginA.invoke() 后置 # 不同拦截对象的执行顺序: # Executor 插件(最外层) # → StatementHandler 插件 # → ParameterHandler 插件 # → ResultSetHandler 插件

自定义插件实战:SQL 性能监控

# 自定义慢 SQL 监控插件 @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}) }) public class SlowSqlPlugin implements Interceptor { // 慢 SQL 阈值(毫秒) private long threshold = 1000; @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { long cost = System.currentTimeMillis() - start; if (cost >= threshold) { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql() .replaceAll("\\s+", " "); log.warn("慢SQL [{}ms]: {}", cost, sql); } } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.threshold = Long.parseLong( properties.getProperty("threshold", "1000")); } } # 注册时设置属性 <plugin interceptor="com.example.SlowSqlPlugin"> <property name="threshold" value="500"/> </plugin>

插件 vs AOP

对比维度MyBatis 插件Spring AOP
拦截层持久层(MyBatis 内部)Service 层/任意 Bean
拦截对象固定的四大对象任意 Spring Bean
粒度SQL 执行过程方法调用
适用场景分页、加密、脱敏、监控日志、事务、权限

实战场景

# 场景1:数据脱敏插件 @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}) }) public class DesensitizePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { List<Object> result = (List<Object>) invocation.proceed(); for (Object obj : result) { // 反射处理敏感字段 Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { Sensitive sensitive = field.getAnnotation(Sensitive.class); if (sensitive != null) { field.setAccessible(true); String value = (String) field.get(obj); field.set(obj, desensitize(value, sensitive.type())); } } } return result; } } # 场景2:数据加密插件(拦截 ParameterHandler 和 ResultSetHandler) # 写入时加密:拦截 ParameterHandler.setParameters() # 读取时解密:拦截 ResultSetHandler.handleResultSets() # 场景3:多数据源自动切换插件 # 拦截 Executor.query()/update() # 根据 ThreadLocal 中的数据源标识 # 动态切换数据源

面试模拟

面试官:MyBatis 插件原理是什么?PageHelper 是怎么实现分页的?

你:MyBatis 插件基于责任链模式 + JDK 动态代理。实现 Interceptor 接口,通过 @Intercepts + @Signature 指定拦截的方法,Plugin.wrap() 创建代理对象。PageHelper 拦截 Executor.query(),通过 ThreadLocal 获取分页参数,先执行 COUNT 查询总记录数,再修改原 SQL 追加 LIMIT 子句实现分页。

面试官:多个插件时执行顺序是怎样的?

你:类似洋葱模型,按注册顺序层层包裹。先注册的是最外层代理,前置先执行、后置后执行。不同类型的拦截对象也有顺序:Executor 最先被拦截,然后是 StatementHandler、ParameterHandler,最后是 ResultSetHandler。