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 执行耗时
下面我们手写一个简单插件,拦截 Executor 的 query 和 update 方法,打印 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中的type、method、args是否与目标对象的方法完全匹配(包括参数类型顺序)。 - 确认插件已正确注册到配置文件中,且没有被其他插件覆盖。
- 注意:MyBatis 二级缓存开启时,部分查询可能不经过 Executor,需要根据情况选择拦截点。
2. 插件能拦截批量操作吗?
可以,但需要正确匹配方法签名。例如 Executor 的 doUpdate 方法在批量执行时被调用,需查看源码确认具体方法。
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。
参考链接: