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。


参考链接

相关推荐
sg_knight2 小时前
如何实现“秒传”与“断点续传”?MinIO + Java 实战进阶篇
java·开发语言·文件管理·minio·ftp·oss·文件传输
William Dawson2 小时前
Java 后端高频 20 题超详细解析 ②
java·开发语言
Flittly2 小时前
【SpringAIAlibaba新手村系列】(15)MCP Client 调用本地服务
java·笔记·spring·ai·springboot
少许极端2 小时前
算法奇妙屋(四十四)-贪心算法学习之路11
java·学习·算法·贪心算法
鱼鳞_2 小时前
Java学习笔记_Day24(HashMAap)
java·笔记·学习
Flittly2 小时前
【SpringAIAlibaba新手村系列】(14)MCP 本地服务与工具集成
java·spring boot·笔记·spring·ai
范什么特西2 小时前
web练习
java·前端·javascript
阿捞22 小时前
JVM排查工具单
java·jvm·python
mfxcyh3 小时前
基于xml、注解、JavaConfig实现spring的ioc
xml·java·spring