MyBatis 插件原理详解:从拦截器到动态代理,手写一个分页插件

MyBatis 插件原理详解:从拦截器到动态代理,手写一个分页插件

插件是 MyBatis 提供的一种强大扩展机制,允许你在 SQL 执行的关键环节进行拦截和增强。本文将深入剖析 MyBatis 插件的底层原理,从四大拦截对象到动态代理,再到手写一个自定义插件,带你彻底搞懂插件机制。


一、插件能做什么?

在实际开发中,MyBatis 插件常被用来实现:

  • 分页查询:自动拦截 SQL,拼接分页方言。
  • 性能监控:记录 SQL 执行耗时。
  • 数据权限:动态修改 SQL 添加过滤条件。
  • 日志打印:输出完整的 SQL 语句和参数。

这些功能之所以能实现,得益于 MyBatis 开放的插件接口 ------ 允许我们在 SQL 执行的关键节点插入自定义逻辑。


二、插件原理核心:拦截器 + 动态代理

MyBatis 插件的本质就是拦截器 。它通过 JDK 动态代理 对核心对象进行包装,在目标方法执行前后插入自定义代码。

一句话总结:

插件 → 实现 Interceptor 接口 → 通过 @Intercepts 注解声明要拦截的对象和方法 → MyBatis 启动时使用 Plugin.wrap() 生成代理对象 → 方法调用时触发拦截逻辑。

核心拦截对象

MyBatis 允许插件拦截以下 4 大核心对象

对象 作用
Executor SQL 执行器,负责整体执行流程(增删改查、缓存管理等)
StatementHandler SQL 语句处理器,负责预编译 SQL、设置参数、执行 SQL 并处理结果集
ParameterHandler 参数处理器,负责为 PreparedStatement 设置参数
ResultSetHandler 结果集处理器,负责将 JDBC 返回的 ResultSet 映射成 Java 对象

注意 :并不是这四个对象的所有方法都能被拦截,只有特定方法(通过 @Signature 指定)才能被拦截。


三、插件的工作机制流程图



MyBatis 初始化
解析配置文件, 注册所有插件
创建四大核心对象 Executor, StatementHandler 等
是否配置了插件?
调用 Plugin.wrap 包装目标对象
返回代理对象, 内部持有拦截器链
返回原始对象
执行 SQL 操作
调用代理对象方法
遍历拦截器链, 依次执行 interceptor.intercept
最终调用原始对象的方法


四、深入源码:插件如何生成代理?

1. 拦截器接口

java 复制代码
public interface Interceptor {
    // 核心拦截逻辑,可在此做增强并调用 invocation.proceed()
    Object intercept(Invocation invocation) throws Throwable;

    // 为目标对象生成代理(默认实现使用 Plugin.wrap)
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 设置插件配置参数
    default void setProperties(Properties properties) {}
}

2. @Intercepts 和 @Signature 注解

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
public @interface Intercepts {
    Signature[] value();
}

@Retention(RetentionPolicy.RUNTIME)
public @interface Signature {
    Class<?> type();      // 四大对象之一:Executor.class 等
    String method();      // 要拦截的方法名,如 "query", "update"
    Class<?>[] args();    // 方法的参数类型(用于区分重载方法)
}

3. Plugin.wrap 的核心逻辑

Plugin 类实现了 InvocationHandler,其 wrap 方法会判断目标对象是否匹配当前插件的拦截签名,若匹配则生成 JDK 动态代理:

java 复制代码
public static Object wrap(Object target, Interceptor interceptor) {
    // 获取拦截器上 @Intercepts 声明的所有 Signature
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 获取目标对象实现的接口(四大对象都是接口实现类)
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
        // 创建动态代理,InvocationHandler 就是 Plugin 自身
        return Proxy.newProxyInstance(type.getClassLoader(),
                                      interfaces,
                                      new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

代理调用时,invoke 方法会判断当前调用的方法是否在签名映射中,若命中则执行拦截器的 intercept 方法,否则直接放行。


五、多个插件的执行顺序(责任链模式)

当配置了多个插件时,MyBatis 会按照配置顺序依次包装目标对象:

java 复制代码
// 假设两个插件:PluginA, PluginB
Object target = new Executor();
target = PluginA.wrap(target);  // 返回代理A,代理A持有 target
target = PluginB.wrap(target);  // 返回代理B,代理B持有代理A

调用时顺序:

代理B → 代理B的intercept → 调用proceed() → 代理A的intercept → 原始对象。

这就是责任链模式,插件可以层层嵌套。


六、自定义插件实战:打印 SQL 执行耗时

下面我们手写一个简单插件,拦截 Executorqueryupdate 方法,打印 SQL 执行耗时。

1. 添加依赖(Spring Boot 示例略)

2. 编写插件类

java 复制代码
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@Intercepts({
    @Signature(type = Executor.class, method = "query",
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update",
               args = {MappedStatement.class, Object.class})
})
public class SqlCostPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();  // 执行原方法
        } finally {
            long end = System.currentTimeMillis();
            System.out.println("SQL executed in " + (end - start) + " ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        // 使用 Plugin.wrap 默认实现即可
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可接收配置参数
        System.out.println("插件配置参数:" + properties);
    }
}

3. 注册插件(mybatis-config.xml)

xml 复制代码
<plugins>
    <plugin interceptor="com.example.SqlCostPlugin">
        <property name="env" value="dev"/>
    </plugin>
</plugins>

若使用 Spring Boot + MyBatis Starter,可通过配置类注入:

java 复制代码
@Configuration
public class MyBatisConfig {
    @Bean
    public SqlCostPlugin sqlCostPlugin() {
        return new SqlCostPlugin();
    }
}

4. 测试效果

执行任意查询或更新,控制台输出:

复制代码
SQL executed in 36 ms

七、常见问题与注意事项

1. 为什么我的插件没有生效?

  • 检查 @Intercepts 中的 typemethodargs 是否与目标对象的方法完全匹配(包括参数类型顺序)。
  • 确认插件已正确注册到配置文件中,且没有被其他插件覆盖。
  • 注意:MyBatis 二级缓存开启时,部分查询可能不经过 Executor,需要根据情况选择拦截点。

2. 插件能拦截批量操作吗?

可以,但需要正确匹配方法签名。例如 ExecutordoUpdate 方法在批量执行时被调用,需查看源码确认具体方法。

3. 拦截后如何获取 SQL 语句?

通过 invocation.getArgs() 获取 MappedStatement 对象,再调用 getBoundSql() 得到 BoundSql,从而获取 SQL 和参数。

4. 多个插件的执行顺序如何保证?

按照 mybatis-config.xml<plugin> 的声明顺序,后声明的插件包裹在先声明的外面,执行时外层先执行,内层后执行。


八、总结

关键点 说明
核心机制 JDK 动态代理 + 责任链模式
拦截对象 Executor、StatementHandler、ParameterHandler、ResultSetHandler
声明方式 实现 Interceptor 接口,使用 @Intercepts@Signature 注解
代理生成 Plugin.wrap() 判断是否匹配签名,匹配则生成代理
执行顺序 按照配置顺序,外层插件先执行
典型应用 分页、监控、数据权限、SQL 打印

掌握 MyBatis 插件原理,不仅能让你更灵活地扩展框架,还能帮助你理解 AOP 和动态代理在实际框架中的应用。如果你想实现更复杂的功能(如分页插件),可以在 intercept 方法中修改 BoundSql,重写原始 SQL。


参考链接

相关推荐
Flittly9 小时前
【AgentScope Java新手村系列】(16)从RAG到多路检索
java·spring boot·spring
小兔崽子去哪了9 小时前
Java 生成二维码解决方案
java·后端
人活一口气14 小时前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
NE_STOP16 小时前
Vibe Coding -- 完整项目案例实操
java
荣码16 小时前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python
SimonKing16 小时前
Google第三方授权登录
java·后端·程序员
明月光81816 小时前
从一行 @Builder 说起:重新拾起 Java 的 Lombok、注解与 Builder 模式
java
考虑考虑1 天前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯1 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
青石路1 天前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java