说说 Mybatis 插件的运行原理?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
设计模式理解:面试官不仅仅是想知道你 "用过" 插件,更是想考察你是否理解 MyBatis 插件底层依赖的动态代理和责任链设计模式。
-
四大核心对象:看你能否准确说出 MyBatis 允许拦截的四大对象(
Executor、StatementHandler、ParameterHandler、ResultSetHandler),以及各自的拦截时机。 -
实战经验:看你能否结合实际场景(如分页插件、SQL 打印、慢 SQL 监控等)说明插件的用途。
核心答案
MyBatis 插件的运行原理可以用一句话概括:基于 JDK 动态代理 + 责任链模式,在四大核心对象的方法执行前后插入自定义逻辑。
| 维度 | 说明 |
|---|---|
| 核心机制 | JDK 动态代理(Proxy + InvocationHandler) |
| 设计模式 | 责任链模式(多个插件层层包装) |
| 可拦截对象 | Executor、StatementHandler、ParameterHandler、ResultSetHandler |
| 接口 | 实现 Interceptor 接口 + @Intercepts 注解 |
| 典型应用 | PageHelper 分页、SQL 打印、慢 SQL 监控、数据权限过滤 |
一句话结论:插件的本质就是对 MyBatis 四大核心对象做一层代理包装,方法调用时走代理逻辑,从而实现在不修改源码的情况下扩展功能。
深度解析
一、四大核心对象及拦截点
MyBatis 在执行一条 SQL 时,会依次经过上面四个核心对象。每个对象都有明确的职责分工:
Executor(执行器):最外层的调度者,负责事务管理、缓存管理、SQL 执行的整体调度。拦截它可以实现分页、SQL 打印等功能。StatementHandler(语句处理器):负责与 JDBC 交互,创建Statement/PreparedStatement并执行 SQL。拦截它可以改写 SQL(比如自动加分页语句)。ParameterHandler(参数处理器):负责将 Java 参数设置到PreparedStatement中。拦截它可以修改参数值或做参数加密等操作。ResultSetHandler(结果集处理器):负责将 JDBCResultSet映射为 Java 对象。拦截它可以做结果过滤、数据脱敏等操作。
二、插件代理的创建过程
插件代理的创建过程遵循责任链模式,层层包装:
- 第一层包装:
StatementHandler原始对象被插件 A(分页插件)包装,生成代理对象proxyA。 - 第二层包装:
proxyA继续被插件 B(SQL 打印插件)包装,生成代理对象proxyB。 - 第三层包装:
proxyB再被插件 C(慢 SQL 监控)包装,生成最终代理对象proxyC。
调用时从最外层代理开始,逐层进入,执行完原始方法后再逐层返回。就像剥洋葱一样,先从外到里,再从里到外。
三、插件实现步骤(代码示例)
实现一个自定义插件只需要 3 步:
第一步:实现 Interceptor 接口
@Intercepts({
// 拦截 StatementHandler 的 prepare 方法
@Signature(
type = StatementHandler.class, // 拦截哪个核心对象
method = "prepare", // 拦截哪个方法
args = {Connection.class, Integer.class} // 方法参数类型
)
})
public class SlowSqlPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 前置逻辑:记录开始时间
long startTime = System.currentTimeMillis();
// 2. 执行原始方法(也可以选择不执行,直接改写逻辑)
Object result = invocation.proceed();
// 3. 后置逻辑:计算耗时,超过阈值则打印警告
long cost = System.currentTimeMillis() - startTime;
if (cost > 1000) {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
System.out.println("[慢 SQL] " + boundSql.getSql() + " | 耗时:" + cost + "ms");
}
return result;
}
@Override
public Object plugin(Object target) {
// 用 Plugin.wrap 生成代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置参数(可选)
}
}
第二步:在 mybatis-config.xml 中注册插件
<plugins>
<plugin interceptor="com.example.plugin.SlowSqlPlugin">
<!-- 可选:传入自定义配置参数 -->
<property name="slowThreshold" value="1000"/>
</plugin>
</plugins>
第三步:MyBatis 自动完成代理创建
MyBatis 在创建四大核心对象时,会遍历所有已注册的插件,依次调用 plugin() 方法对目标对象进行包装,最终生成代理对象。
四、源码级执行流程
关键源码在 Plugin 类中,它实现了 InvocationHandler 接口:
// Plugin.java(简化版,展示核心逻辑)
public class Plugin implements InvocationHandler {
private Object target; // 被代理的原始对象
private Interceptor interceptor; // 插件实现类
private Map<Class<?>, Set<Method>> signatureMap; // 拦截签名映射
// 创建代理对象
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 解析 @Intercepts 注解,获取要拦截的方法签名
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 2. 判断当前 target 是否需要被代理
Class<?> type = target.getClass();
if (signatureMap.containsKey(type.getInterfaces()[0])) {
// 3. 创建 JDK 动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
type.getInterfaces(),
new Plugin(target, interceptor, signatureMap)
);
}
// 不需要代理,直接返回原始对象
return target;
}
// 代理方法调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 检查当前方法是否在拦截签名中
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 2. 命中拦截 → 调用插件的 intercept() 方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 3. 未命中拦截 → 直接调用原始方法
return method.invoke(target, args);
}
}
整个流程可以简化为:
wrap()→ 解析注解,判断是否需要代理 → 创建Proxy代理对象invoke()→ 方法调用时,检查是否在拦截范围内 → 命中则走interceptor.intercept(),未命中则直接执行原始方法
五、常见误区
-
误区一:"插件能拦截所有方法" — 不行,只能拦截四大核心对象(
Executor、StatementHandler、ParameterHandler、ResultSetHandler)的指定方法。拦截Mapper接口的方法是不行的。 -
误区二:"插件越多越好" — 插件基于动态代理,每加一个插件就多一层代理嵌套,方法调用栈会更深,会影响性能。生产环境应控制插件数量。
-
误区三:"
intercept()必须调用proceed()" — 不一定。如果你需要完全替换原始逻辑(比如改写 SQL),可以不调用proceed(),直接返回自定义结果。但大多数场景下应该调用proceed()以保证原始逻辑正常执行。
面试高频追问
- 追问一:MyBatis 分页插件的原理是什么?
- 以 PageHelper 为例,它拦截
Executor的query方法,在 SQL 执行前自动拼接LIMIT语句(MySQL)或ROWNUM(Oracle),同时执行一条COUNT(*)查询获取总记录数。
- 以 PageHelper 为例,它拦截
- 追问二:插件之间有执行顺序吗?
- 有。在
mybatis-config.xml中,插件按注册顺序依次包装目标对象,先注册的在内层,后注册的在外层。调用时从外层开始,类似 Spring 拦截器的 "洋葱模型"。
- 有。在
- 追问三:为什么不建议写太多自定义插件?
- 两个原因:一是每增加一个插件就多一层动态代理,影响调用栈深度和性能;二是插件直接操作 MyBatis 内部对象,版本升级时可能不兼容。
常见面试变体
- 变体一:"MyBatis 的拦截器(Interceptor)怎么用?"
- 变体二:"MyBatis 插件能拦截哪些对象?"
- 变体三:"说说 PageHelper 分页插件的实现原理?"
记忆口诀
四大对象层层调,插件代理来包装;动态代理 InvocationHandler,责任链模式洋葱套;拦截要看 @Signature,proceed() 调用别忘掉。
总结
MyBatis 插件的运行原理是基于 JDK 动态代理 + 责任链模式,在四大核心对象(Executor → StatementHandler → ParameterHandler → ResultSetHandler)创建时,通过 Plugin.wrap() 生成代理对象,方法调用时走 invoke() 判断是否命中拦截签名,命中则执行自定义 intercept() 逻辑。典型应用包括分页插件、SQL 打印、慢 SQL 监控等。