第5篇-插件机制与责任链
约 4341 字大约 14 分钟
2026-05-24
四、InterceptorChain:插件的责任链
MyBatis 使用 InterceptorChain 来管理所有的插件。
1. InterceptorChain 源码
类路径:org.apache.ibatis.plugin.InterceptorChain
/**
* @author Clinton Begin
*/
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
/**
* 【核心】应用所有插件
* 这是形成责任链的关键方法!
*
* 流程:
* 1. 遍历所有插件
* 2. 依次调用每个插件的 plugin() 方法
* 3. 每个插件都会为目标对象创建代理对象
* 4. 如果有 3 个插件,就会形成 3 层代理
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
/**
* 添加插件
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/**
* 获取所有插件
*/
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}关键点:
- pluginAll():遍历所有插件,依次调用
plugin()方法,形成责任链 - addInterceptor():添加插件(在 MyBatis 初始化时调用)
你可能会问:pluginAll() 是如何形成责任链的呢?
好问题! 让我用一个例子来解释:
假设你有 3 个插件:pluginA、pluginB、pluginC
// 原始对象
Object target = executor;
// 第一次循环:pluginA.plugin(target)
// 创建代理对象:PluginAProxy -> target(executor)
target = Plugin.wrap(target, pluginA);
// 第二次循环:pluginB.plugin(target)
// 创建代理对象:PluginBProxy -> target(PluginAProxy)
target = Plugin.wrap(target, pluginB);
// 第三次循环:pluginC.plugin(target)
// 创建代理对象:PluginCProxy -> target(PluginBProxy)
target = Plugin.wrap(target, pluginC);
// 最终:PluginCProxy -> PluginBProxy -> PluginAProxy -> executor看到了吗? 这就是责任链的形成过程!每个插件都会为目标对象创建一层代理。
五、Plugin 类:动态代理的核心实现
Plugin 类是 MyBatis 插件机制的核心!它实现了 InvocationHandler 接口,负责创建动态代理对象。
1. Plugin 类源码(完整)
类路径:org.apache.ibatis.plugin.Plugin
/**
* @author Clinton Begin
*/
public class Plugin implements InvocationHandler {
private final Object target; // 目标对象(被代理的对象)
private final Interceptor interceptor; // 拦截器(插件)
private final Map<Class<?>, Set<Method>> signatureMap; // 方法签名映射
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
/**
* 【核心】创建动态代理对象
* 这是 MyBatis 插件机制的核心方法!
*
* 流程:
* 1. 解析插件的 @Intercepts 注解,获取方法签名映射
* 2. 获取目标对象实现的所有接口
* 3. 使用 JDK 动态代理创建代理对象
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 【核心】解析 @Intercepts 注解,获取方法签名映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 【核心】获取目标对象实现的所有接口(只保留需要拦截的接口)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 【核心】创建 JDK 动态代理对象
// 代理对象的 InvocationHandler 就是 Plugin 本身
return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
// 如果没有需要拦截的接口,返回原始对象
return target;
}
/**
* 【核心】动态代理的 invoke() 方法
* 当调用代理对象的方法时,会执行这个方法
*
* 流程:
* 1. 判断当前方法是否需要拦截
* 2. 如果需要拦截,调用 interceptor.intercept() 方法
* 3. 如果不需要拦截,直接调用原始方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 【核心】判断当前方法是否需要拦截
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 【核心】需要拦截,调用插件的 intercept() 方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 【注意】不需要拦截,直接调用原始方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
/**
* 【核心】解析 @Intercepts 注解,获取方法签名映射
* 比如:{Executor.class -> [query(), update()], StatementHandler.class -> [prepare()]}
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 【核心】获取 @Intercepts 注解
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251:必须要有 @Intercepts 注解
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
// 【核心】遍历所有 @Signature 注解
for (Signature sig : sigs) {
// 【注意】获取或创建方法集合
Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
try {
// 【核心】获取方法对象(通过方法名和参数类型)
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
/**
* 【核心】获取目标对象实现的所有接口(只保留需要拦截的接口)
* 比如:target 实现了 Executor 和 Serializable 接口
* 但 @Intercepts 只指定了 Executor,所以只返回 Executor 接口
*/
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[0]);
}
}哇! 是不是感觉 Plugin 类非常精妙?
关键点:
- wrap():创建动态代理对象(使用 JDK 动态代理)
- invoke():判断当前方法是否需要拦截,如果需要就调用插件的
intercept()方法 - getSignatureMap():解析
@Intercepts注解,获取方法签名映射 - getAllInterfaces():获取目标对象实现的所有接口(只保留需要拦截的接口)
你可能会问:为什么使用 JDK 动态代理,而不是 CGLIB?
问得太好了! 因为 MyBatis 的拦截目标是接口(Executor、StatementHandler、ParameterHandler、ResultSetHandler 都是接口),所以使用 JDK 动态代理就可以了。
六、Invocation 类:封装原始方法调用
Invocation 类是一个简单的封装类,它封装了原始方法的调用信息。
1. Invocation 类源码
类路径:org.apache.ibatis.plugin.Invocation
/**
* @author Clinton Begin
*/
public class Invocation {
private final Object target; // 目标对象(被代理的对象)
private final Method method; // 原始方法
private final Object[] args; // 方法参数
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
/**
* 【核心】调用原始方法
* 这是责任链继续执行的关键!
*
* 比如:
* 1. 插件A.intercept() 调用 invocation.proceed()
* 2. 继续执行插件B.intercept()
* 3. 继续执行插件C.intercept()
* 4. 最终执行原始方法
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
}关键点:
- proceed():调用原始方法,继续执行责任链
- getArgs():获取方法参数(比如
MappedStatement、Object parameter、RowBounds等)
你可能会问:Invocation.getArgs() 有什么用?
好问题! 你可以通过 getArgs() 获取方法参数,然后修改它们!
比如,分页插件就是通过修改 args[2](RowBounds)来实现分页的!
七、如何自定义插件(示例:SQL 执行时间监控插件)
好了,现在我们已经把 MyBatis 插件机制的核心源码都讲完了。
你可能会问:我知道原理了,但是怎么自定义插件呢?
别着急,我来给你演示一个完整的示例:SQL 执行时间监控插件。
1. 示例:SQL 执行时间监控插件
/**
* SQL 执行时间监控插件
* 功能:统计每个 SQL 的执行时间,并打印到控制台
*
* 【核心】通过 @Intercepts 注解指定要拦截的组件和方法
*/
@Intercepts({
// 【注意】拦截 Executor 的 query() 方法(查询)
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
// 【注意】拦截 Executor 的 update() 方法(更新)
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class SqlExecutionTimePlugin implements Interceptor {
private long threshold = 1000; // 默认阈值:1000ms
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 【核心】获取方法参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
// 【核心】获取 SQL 语句
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 【核心】记录开始时间
long start = System.currentTimeMillis();
try {
// 【核心】继续执行责任链(最终会执行原始方法)
Object result = invocation.proceed();
// 【核心】记录结束时间
long end = System.currentTimeMillis();
long executionTime = end - start;
// 【注意】如果执行时间超过阈值,打印警告日志
if (executionTime > threshold) {
System.out.println("【慢 SQL 警告】执行时间:" + executionTime + "ms,SQL:" + sql);
} else {
System.out.println("【SQL 执行时间】" + executionTime + "ms,SQL:" + sql);
}
return result;
} catch (Throwable t) {
// 【注意】如果抛出异常,也记录执行时间
long end = System.currentTimeMillis();
System.out.println("【SQL 执行失败】执行时间:" + (end - start) + "ms,SQL:" + sql);
throw t;
}
}
@Override
public Object plugin(Object target) {
// 【核心】使用 Plugin.wrap() 创建代理对象
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 【核心】读取配置属性(比如阈值)
String threshold = properties.getProperty("threshold");
if (threshold != null) {
this.threshold = Long.parseLong(threshold);
}
}
}2. 注册插件(MyBatis 配置文件)
<!-- mybatis-config.xml -->
<configuration>
<plugins>
<!-- 【核心】注册 SQL 执行时间监控插件 -->
<plugin interceptor="com.example.SqlExecutionTimePlugin">
<property name="threshold" value="500"/>
</plugin>
</plugins>
</configuration>3. 注册插件(Spring Boot 配置)
如果你用的是 Spring Boot,可以在配置类中注册插件:
/**
* MyBatis 配置类
*/
@Configuration
public class MyBatisConfig {
/**
* 【核心】注册 SQL 执行时间监控插件
*/
@Bean
public SqlExecutionTimePlugin sqlExecutionTimePlugin() {
SqlExecutionTimePlugin plugin = new SqlExecutionTimePlugin();
Properties properties = new Properties();
properties.setProperty("threshold", "500");
plugin.setProperties(properties);
return plugin;
}
}看到了吗? 自定义插件就是这么简单!
八、插件的应用场景
MyBatis 插件的应用场景非常广泛,下面我来介绍几个常见的场景:
1. 分页插件(比如 PageHelper)
功能:自动在 SQL 后面加上 LIMIT ? OFFSET ?
实现原理:
- 拦截
Executor.query()方法 - 获取
RowBounds参数(分页参数) - 修改 SQL,加上
LIMIT ? OFFSET ? - 继续执行责任链
示例:
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageHelperPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 【核心】获取 RowBounds(分页参数)
RowBounds rowBounds = (RowBounds) invocation.getArgs()[2];
if (rowBounds != null && rowBounds != RowBounds.DEFAULT) {
// 【核心】修改 SQL,加上 LIMIT 和 OFFSET
// 这里需要解析 SQL,然后加上分页参数
// 具体实现比较复杂,可以参考 PageHelper 的源码
}
return invocation.proceed();
}
}2. SQL 监控插件
功能:统计每个 SQL 的执行时间、执行次数、平均执行时间等
实现原理:
- 拦截
Executor.query()和Executor.update()方法 - 在方法执行前记录开始时间
- 在方法执行后记录结束时间
- 统计执行时间,并打印日志或发送到监控系统
(示例就是上面那个 SqlExecutionTimePlugin)
3. 数据加解密插件
功能:自动加密敏感字段(比如手机号、身份证号),查询时自动解密
实现原理:
- 拦截
ParameterHandler.setParameters()方法(加密) - 拦截
ResultSetHandler.handleResultSets()方法(解密)
示例:
@Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters",
args = {PreparedStatement.class})})
public class EncryptionPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 【核心】获取 PreparedStatement
PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];
// 【核心】加密敏感字段
// 这里需要获取参数对象,然后加密敏感字段
// 具体实现需要根据你的业务需求来定
return invocation.proceed();
}
}4. 逻辑删除插件
功能:自动加上 WHERE deleted = 0,避免删除的数据被查询出来
实现原理:
- 拦截
Executor.query()方法 - 修改 SQL,加上
WHERE deleted = 0
示例:
@Intercepts({@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class LogicalDeletePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 【核心】获取 MappedStatement
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
// 【核心】获取 BoundSql
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = ms.getBoundSql(parameter);
// 【核心】修改 SQL,加上 WHERE deleted = 0
String sql = boundSql.getSql();
if (!sql.contains("deleted")) {
sql = sql + " WHERE deleted = 0";
// 【注意】这里需要修改 BoundSql 的 sql 属性
// 具体实现比较复杂,可以参考 MyBatis-Plus 的源码
}
return invocation.proceed();
}
}九、插件的注意事项
虽然 MyBatis 插件很强大,但是使用时有几个注意事项:
1. 插件顺序
你可能会问:如果我有多个插件,它们的执行顺序是什么?
答案:插件的执行顺序取决于它们在 mybatis-config.xml 中的注册顺序!
比如:
<plugins>
<plugin interceptor="com.example.PluginA"/>
<plugin interceptor="com.example.PluginB"/>
<plugin interceptor="com.example.PluginC"/>
</plugins>那么,执行顺序是:
PluginA.intercept()
↓
PluginB.intercept()
↓
PluginC.intercept()
↓
原始方法
↑
PluginC.intercept() 返回
↑
PluginB.intercept() 返回
↑
PluginA.intercept() 返回看到了吗? 先注册的插件,在外层;后注册的插件,在内层。
2. 性能影响
你可能会问:插件会影响性能吗?
答案:会!因为插件使用了动态代理,每次方法调用都会经过代理对象。
但是,影响通常很小,可以忽略不计。除非你的插件逻辑非常复杂(比如解析 SQL、修改 SQL),否则性能影响可以忽略。
3. 不要过度使用插件
你可能会问:我可以拦截所有组件的所有方法吗?
答案:不建议!因为:
- 插件越多,责任链越长,性能影响越大
- 插件逻辑越复杂,越容易出现 bug
- 插件可能会影响 MyBatis 的原有行为,导致不可预期的结果
所以,建议你只使用必要的插件,并且保持插件逻辑简单。
十、Spring Boot 自动装配入口:MybatisAutoConfiguration
按照我们的约定,每个系列都必须包含 Spring Boot 自动装配的核心入口代码解读。
在系列三中,我们需要解读 MybatisAutoConfiguration。但是,MybatisAutoConfiguration 并没有直接配置插件。
那么,插件是在哪里注册的呢?
答案:在 SqlSessionFactoryBean 中!
1. 注册插件的方式(Spring Boot)
如果你用的是 Spring Boot,有两种方式注册插件:
方式一:在配置类中创建插件 Bean
@Configuration
public class MyBatisConfig {
@Bean
public SqlExecutionTimePlugin sqlExecutionTimePlugin() {
SqlExecutionTimePlugin plugin = new SqlExecutionTimePlugin();
Properties properties = new Properties();
properties.setProperty("threshold", "500");
plugin.setProperties(properties);
return plugin;
}
}注意:MyBatis-Spring 会自动扫描所有实现了 Interceptor 接口的 Bean,并注册到 InterceptorChain 中!
方式二:在 application.yml 中配置插件(不推荐)
MyBatis-Spring-Boot-Starter 不支持在 application.yml 中配置插件。你必须在配置类中创建插件 Bean。
2. SqlSessionFactoryBean 如何注册插件
类路径:org.mybatis.spring.SqlSessionFactoryBean
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
private Interceptor[] plugins; // 【核心】插件数组
/**
* 【核心】设置插件
* Spring 会自动调用这个方法,注入所有 Interceptor Bean
*/
public void setPlugins(Interceptor[] plugins) {
this.plugins = plugins;
}
/**
* 【核心】构建 SqlSessionFactory
*/
@Override
public SqlSessionFactory getObject() throws Exception {
// ... 省略 ...
// 【核心】注册插件
if (plugins != null) {
for (Interceptor plugin : plugins) {
configuration.addInterceptor(plugin);
}
}
// ... 省略 ...
return sqlSessionFactory;
}
}看到了吗? SqlSessionFactoryBean 会调用 configuration.addInterceptor(plugin) 来注册插件!
十一、总结与系列三的总结
1. 本文总结
今天,我们深入剖析了 MyBatis 的 插件机制与责任链,包括:
- MyBatis 插件机制的核心原理:动态代理 + 责任链
- Interceptor 接口:插件必须实现的接口
- @Intercepts 和 @Signature 注解:指定拦截点
- InterceptorChain:插件的 responsibility chain
- Plugin 类:动态代理的核心实现
- Invocation 类:封装原始方法调用
- 如何自定义插件:示例(SQL 执行时间监控插件)
- 插件的应用场景:分页插件、SQL 监控插件、数据加解密插件、逻辑删除插件
- 插件的注意事项:插件顺序、性能影响、不要过度使用
2. 系列三的总结
到这里,MyBatis 系列(5 篇文章)就全部讲完了!让我们来回顾一下:
第 1 篇:从 Mapper 接口到 SQL 执行的奇幻漂流
- MapperProxy 的动态代理原理
- MapperMethod 如何把接口方法转换成 SQL
- SqlSessionTemplate 如何整合 Spring
第 2 篇:Executor 执行器的三种形态
- SimpleExecutor:每次创建新的 Statement
- ReuseExecutor:复用 Statement
- BatchExecutor:批处理
- Spring Boot 自动装配入口(MybatisAutoConfiguration)
第 3 篇:事务管理 —— SpringManagedTransaction 的秘密
- MyBatis 如何在 Spring 事务环境中获取连接
- DataSourceUtils.getConnection() 的奥秘
- 事务同步管理器的实现原理
第 4 篇:SQL 执行的全链路
- StatementHandler:SQL 执行的指挥官
- ParameterHandler:参数设置的魔术师
- ResultSetHandler:结果集转换的艺术家
第 5 篇:插件机制与责任链(本文)
- MyBatis 插件机制的核心原理
- 如何自定义插件
- 插件的应用场景和注意事项
看到了吗? 这 5 篇文章覆盖了 MyBatis 的全生命周期!
从 Mapper 接口调用,到 SQL 执行,再到结果集处理,最后到插件扩展,我们已经把 MyBatis 的核心原理都讲透了!
你可能会问:接下来呢?还有什么要讲的吗?
当然有! 接下来,我们会进入系列四:总结系列。
在总结系列中,我们会:
- 从更高的角度串联 MyBatis、Spring 事务、数据源、MySQL 驱动,形成完整的全链路
- 分析每个环节可能出现的异常,并给出排查方法
- 基于源码的调优实践(事务调优、数据源调优、MyBatis 调优)
- 那些年我们踩过的事务坑(自调用、异常不回滚、连接泄漏等)
是不是很期待? 😎
十二、下一篇预告
在下一篇(系列四第 1 篇)中,我们会讲解 《从 @Transactional 到 MySQL 返回的完整旅程》。
你会学到:
- 从 @Transactional 注解开始,到 MySQL 返回结果,完整的全链路时序图
- 每个环节都发生了什么?涉及哪些核心类?
- 如何基于这个全链路进行问题排查和性能调优?
你可能会问:为什么需要总结系列?直接看前面的文章不行吗?
问得好! 因为前面的文章是自顶向下的,每篇文章都专注于一个特定的主题。但是,在实际工作中,你需要从全局视角来看待问题。
比如,当你遇到一个事务不回滚的问题时,你需要:
- 检查 @Transactional 注解的配置(系列一)
- 检查数据源配置(系列二)
- 检查 MyBatis 的事务管理(系列三)
- 基于全链路进行排查(系列四)
所以,总结系列就是帮你把前面的知识串联起来,形成一个完整的知识体系!
相信我,这会是一次非常有趣、非常有收获的旅程!🚀
写到这里,我不禁感叹:MyBatis 的设计真的是太精妙了!每一个组件都职责清晰,易于扩展。特别是插件机制,简直是开闭原则(OCP)的完美体现!
好了,今天的文章就到这里。如果你觉得有收获,别忘了点赞、收藏、关注哦!你的支持,是我创作的最大动力!❤️
我们下一篇见!👋