说说 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 插件底层依赖的动态代理责任链设计模式。

  2. 四大核心对象:看你能否准确说出 MyBatis 允许拦截的四大对象(ExecutorStatementHandlerParameterHandlerResultSetHandler),以及各自的拦截时机。

  3. 实战经验:看你能否结合实际场景(如分页插件、SQL 打印、慢 SQL 监控等)说明插件的用途。

核心答案

MyBatis 插件的运行原理可以用一句话概括:基于 JDK 动态代理 + 责任链模式,在四大核心对象的方法执行前后插入自定义逻辑

维度说明
核心机制JDK 动态代理(Proxy + InvocationHandler
设计模式责任链模式(多个插件层层包装)
可拦截对象ExecutorStatementHandlerParameterHandlerResultSetHandler
接口实现 Interceptor 接口 + @Intercepts 注解
典型应用PageHelper 分页、SQL 打印、慢 SQL 监控、数据权限过滤

一句话结论:插件的本质就是对 MyBatis 四大核心对象做一层代理包装,方法调用时走代理逻辑,从而实现在不修改源码的情况下扩展功能。

深度解析

一、四大核心对象及拦截点

MyBatis 在执行一条 SQL 时,会依次经过上面四个核心对象。每个对象都有明确的职责分工:

  • Executor(执行器):最外层的调度者,负责事务管理、缓存管理、SQL 执行的整体调度。拦截它可以实现分页、SQL 打印等功能。
  • StatementHandler(语句处理器):负责与 JDBC 交互,创建 Statement / PreparedStatement 并执行 SQL。拦截它可以改写 SQL(比如自动加分页语句)。
  • ParameterHandler(参数处理器):负责将 Java 参数设置到 PreparedStatement 中。拦截它可以修改参数值或做参数加密等操作。
  • ResultSetHandler(结果集处理器):负责将 JDBC ResultSet 映射为 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(),未命中则直接执行原始方法

五、常见误区

  1. 误区一:"插件能拦截所有方法" — 不行,只能拦截四大核心对象(ExecutorStatementHandlerParameterHandlerResultSetHandler)的指定方法。拦截 Mapper 接口的方法是不行的。

  2. 误区二:"插件越多越好" — 插件基于动态代理,每加一个插件就多一层代理嵌套,方法调用栈会更深,会影响性能。生产环境应控制插件数量。

  3. 误区三:"intercept() 必须调用 proceed()" — 不一定。如果你需要完全替换原始逻辑(比如改写 SQL),可以不调用 proceed(),直接返回自定义结果。但大多数场景下应该调用 proceed() 以保证原始逻辑正常执行。

面试高频追问

  1. 追问一:MyBatis 分页插件的原理是什么?
    • 以 PageHelper 为例,它拦截 Executorquery 方法,在 SQL 执行前自动拼接 LIMIT 语句(MySQL)或 ROWNUM(Oracle),同时执行一条 COUNT(*) 查询获取总记录数。
  2. 追问二:插件之间有执行顺序吗?
    • 有。在 mybatis-config.xml 中,插件按注册顺序依次包装目标对象,先注册的在内层,后注册的在外层。调用时从外层开始,类似 Spring 拦截器的 "洋葱模型"。
  3. 追问三:为什么不建议写太多自定义插件?
    • 两个原因:一是每增加一个插件就多一层动态代理,影响调用栈深度和性能;二是插件直接操作 MyBatis 内部对象,版本升级时可能不兼容。

常见面试变体

  • 变体一:"MyBatis 的拦截器(Interceptor)怎么用?"
  • 变体二:"MyBatis 插件能拦截哪些对象?"
  • 变体三:"说说 PageHelper 分页插件的实现原理?"

记忆口诀

四大对象层层调,插件代理来包装;动态代理 InvocationHandler,责任链模式洋葱套;拦截要看 @Signature,proceed() 调用别忘掉。

总结

MyBatis 插件的运行原理是基于 JDK 动态代理 + 责任链模式,在四大核心对象(ExecutorStatementHandlerParameterHandlerResultSetHandler)创建时,通过 Plugin.wrap() 生成代理对象,方法调用时走 invoke() 判断是否命中拦截签名,命中则执行自定义 intercept() 逻辑。典型应用包括分页插件、SQL 打印、慢 SQL 监控等。