说说 Mybatis 插件的运行原理?
2026年01月06日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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 架构核心组件的理解深度:是否了解
Executor、StatementHandler、ParameterHandler、ResultSetHandler这四大核心对象在 SQL 执行过程中的作用与协作关系。 - 对框架扩展机制的设计洞察力:不仅仅是知道如何写一个插件,更要理解 MyBatis 是如何开放扩展点,以及其设计背后的考量(开闭原则、无侵入性)。
- 对设计模式的实际应用能力:能否清晰地阐述责任链模式和动态代理在此场景下的具体应用。这是面试官最想听到的底层原理。
- 源码阅读与原理阐述能力:能否将复杂的运行流程(配置加载、代理创建、链式调用)用简洁清晰的语言描述出来。
核心答案
MyBatis 插件的运行原理核心是 动态代理 和 责任链模式。它允许我们拦截并增强 MyBatis 四大核心对象 (Executor, StatementHandler, ParameterHandler, ResultSetHandler) 的方法。
其工作流程是:MyBatis 在启动时,会读取配置的插件,并通过 Plugin.wrap() 方法,使用 JDK 动态代理,将目标对象层层包裹。当调用被代理对象的方法时,会先进入 Plugin.invoke() 方法。该方法会判断当前被调用的方法是否为配置中声明需要拦截的方法。如果是,则按插件配置顺序,依次调用所有适用插件的 intercept() 方法,形成一个拦截器调用链;如果不是,则直接调用原方法。
深度解析
原理/机制
-
核心拦截对象: MyBatis 插件只能拦截这四大接口定义的方法:
Executor:执行器,负责增删改查操作,是插件拦截最核心的入口(可拦截缓存、提交等)。StatementHandler:负责处理 JDBCStatement,可拦截 SQL 语法构建、参数设置、执行等。ParameterHandler:负责将 Java 参数转换为 JDBC 参数。ResultSetHandler:负责将 JDBC 返回的ResultSet转换为 Java 对象列表。
-
插件加载与代理创建流程:
- 配置解析:MyBatis 在初始化
Configuration时,会解析<plugins>配置,实例化所有声明的拦截器,并存入InterceptorChain(一个存储Interceptor的列表)。 - 责任链组装:在创建上述四大对象时(例如在
newExecutor、newStatementHandler等方法中),Configuration会调用InterceptorChain.pluginAll()方法。 - 动态代理包装:
pluginAll()会遍历所有拦截器,并调用每个拦截器的plugin()方法。通常我们使用 MyBatis 提供的Plugin.wrap(target, this)来创建代理。wrap()方法会获取拦截器注解@Intercepts中声明的签名,然后为目标对象创建一个实现了其所有接口的 JDK 动态代理对象(Plugin类本身实现了InvocationHandler)。 - 层层包裹:插件按配置顺序依次对目标对象进行代理。如果配置了多个插件,最终的对象将是经过多层代理包裹后的对象,执行顺序是 配置顺序的正序,而执行时的调用链则是从最外层代理逐层向内。
- 配置解析:MyBatis 在初始化
代码示例
一个典型的分页插件拦截 StatementHandler.prepare 方法的骨架:
// 1. 定义拦截器,使用 @Intercepts 注解声明要拦截的方法签名
@Intercepts({
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})
})
public class MyPaginationInterceptor implements Interceptor {
// 2. 核心拦截方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取被代理的原始对象(StatementHandler)
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 3. 对 SQL 进行改写(例如,添加 LIMIT 子句)
String paginatedSql = originalSql + " LIMIT ?, ?";
// 利用反射修改 BoundSql 中的 SQL 语句
Field sqlField = BoundSql.class.getDeclaredField("sql");
sqlField.setAccessible(true);
sqlField.set(boundSql, paginatedSql);
// 4. 调用 invocation.proceed(),将执行权交给责任链中的下一个拦截器,最终执行原方法
return invocation.proceed();
}
// 5. 用当前拦截器包装目标对象,返回代理对象
@Override
public Object plugin(Object target) {
// 使用 MyBatis 提供的工具方法创建代理
return Plugin.wrap(target, this);
}
// 6. 设置插件属性(从配置文件中读取)
@Override
public void setProperties(Properties properties) {
// 读取配置参数...
}
}
最佳实践与常见误区
- 最佳实践:
- 谨慎选择拦截点:根据功能选择最合适的拦截对象。修改 SQL 应在
StatementHandler.prepare阶段;修改参数应在ParameterHandler.setParameters;影响整体执行过程(如缓存)应在Executor层面。 - 保持轻量:
intercept方法中的逻辑应高效,避免影响 MyBatis 核心执行性能。 - 处理好线程安全:拦截器通常是单例的,如果其内部有成员变量,需考虑线程安全。
- 谨慎选择拦截点:根据功能选择最合适的拦截对象。修改 SQL 应在
- 常见误区:
- 误认为插件可以拦截任意方法:只能拦截四大接口声明的方法。
- 不理解代理顺序与执行顺序的关系:虽然代理是“正序”层层包裹,但执行时是从外到内(正序),而
proceed()的调用是向链的深处推进。 - 在
plugin()方法中直接返回target:这样会导致插件完全失效,必须返回Plugin.wrap()创建的代理对象。
总结
MyBatis 插件通过 JDK 动态代理 和责任链模式,无侵入地拦截并增强四大核心组件的方法,其本质是在目标方法执行路径上动态织入自定义逻辑,是框架扩展性的关键设计。理解这一原理,对于编写高效、稳定的插件以及深度定制 MyBatis 行为至关重要。