说说 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/

面试考察点

面试官提出这个问题,主要希望考察你以下几个层面的理解:

  1. 对 MyBatis 架构核心组件的理解深度:是否了解 ExecutorStatementHandlerParameterHandlerResultSetHandler 这四大核心对象在 SQL 执行过程中的作用与协作关系。
  2. 对框架扩展机制的设计洞察力:不仅仅是知道如何写一个插件,更要理解 MyBatis 是如何开放扩展点,以及其设计背后的考量(开闭原则、无侵入性)。
  3. 对设计模式的实际应用能力:能否清晰地阐述责任链模式动态代理在此场景下的具体应用。这是面试官最想听到的底层原理。
  4. 源码阅读与原理阐述能力:能否将复杂的运行流程(配置加载、代理创建、链式调用)用简洁清晰的语言描述出来。

核心答案

MyBatis 插件的运行原理核心是 动态代理责任链模式。它允许我们拦截并增强 MyBatis 四大核心对象 (Executor, StatementHandler, ParameterHandler, ResultSetHandler) 的方法。

其工作流程是:MyBatis 在启动时,会读取配置的插件,并通过 Plugin.wrap() 方法,使用 JDK 动态代理,将目标对象层层包裹。当调用被代理对象的方法时,会先进入 Plugin.invoke() 方法。该方法会判断当前被调用的方法是否为配置中声明需要拦截的方法。如果是,则按插件配置顺序,依次调用所有适用插件的 intercept() 方法,形成一个拦截器调用链;如果不是,则直接调用原方法。

深度解析

原理/机制

  1. 核心拦截对象: MyBatis 插件只能拦截这四大接口定义的方法:

    • Executor:执行器,负责增删改查操作,是插件拦截最核心的入口(可拦截缓存、提交等)。
    • StatementHandler:负责处理 JDBC Statement,可拦截 SQL 语法构建、参数设置、执行等。
    • ParameterHandler:负责将 Java 参数转换为 JDBC 参数。
    • ResultSetHandler:负责将 JDBC 返回的 ResultSet 转换为 Java 对象列表。
  2. 插件加载与代理创建流程

    • 配置解析:MyBatis 在初始化 Configuration 时,会解析 <plugins> 配置,实例化所有声明的拦截器,并存入 InterceptorChain(一个存储 Interceptor 的列表)。
    • 责任链组装:在创建上述四大对象时(例如在 newExecutornewStatementHandler 等方法中),Configuration 会调用 InterceptorChain.pluginAll() 方法。
    • 动态代理包装pluginAll() 会遍历所有拦截器,并调用每个拦截器的 plugin() 方法。通常我们使用 MyBatis 提供的 Plugin.wrap(target, this) 来创建代理。wrap() 方法会获取拦截器注解 @Intercepts 中声明的签名,然后为目标对象创建一个实现了其所有接口的 JDK 动态代理对象(Plugin 类本身实现了 InvocationHandler)。
    • 层层包裹:插件按配置顺序依次对目标对象进行代理。如果配置了多个插件,最终的对象将是经过多层代理包裹后的对象,执行顺序是 配置顺序的正序,而执行时的调用链则是从最外层代理逐层向内

代码示例

一个典型的分页插件拦截 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) {
        // 读取配置参数...
    }
}

最佳实践与常见误区

  • 最佳实践
    1. 谨慎选择拦截点:根据功能选择最合适的拦截对象。修改 SQL 应在 StatementHandler.prepare 阶段;修改参数应在 ParameterHandler.setParameters;影响整体执行过程(如缓存)应在 Executor 层面。
    2. 保持轻量intercept 方法中的逻辑应高效,避免影响 MyBatis 核心执行性能。
    3. 处理好线程安全:拦截器通常是单例的,如果其内部有成员变量,需考虑线程安全。
  • 常见误区
    1. 误认为插件可以拦截任意方法:只能拦截四大接口声明的方法。
    2. 不理解代理顺序与执行顺序的关系:虽然代理是“正序”层层包裹,但执行时是从外到内(正序),而 proceed() 的调用是向链的深处推进。
    3. plugin() 方法中直接返回 target:这样会导致插件完全失效,必须返回 Plugin.wrap() 创建的代理对象。

总结

MyBatis 插件通过 JDK 动态代理 和责任链模式,无侵入地拦截并增强四大核心组件的方法,其本质是在目标方法执行路径上动态织入自定义逻辑,是框架扩展性的关键设计。理解这一原理,对于编写高效、稳定的插件以及深度定制 MyBatis 行为至关重要。