多插件执行顺序
MyBatis 支持多个插件同时注册,插件在配置文件中的声明顺序决定拦截链的执行顺序,理解顺序机制是编写正确插件的前提。
插件注册顺序
XML 配置声明顺序
XML
<configuration>
<plugins>
<plugin interceptor="com.example.PluginA">
<property name="name" value="A"/>
</plugin>
<plugin interceptor="com.example.PluginB">
<property name="name" value="B"/>
</plugin>
<plugin interceptor="com.example.PluginC">
<property name="name" value="C"/>
</plugin>
</plugins>
</configuration>
注册顺序:A → B → C,构建代理链时按此顺序逐层包装。
注册源码
Java
// XMLConfigBuilder.java
private void pluginElement(XNode parent) throws Exception {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor instance = (Interceptor) resolveClass(interceptor)
.getDeclaredConstructor().newInstance();
instance.setProperties(properties);
// ★ 按 XML 声明顺序依次添加到 InterceptorChain
configuration.addInterceptor(instance);
}
}
// Configuration.java
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
InterceptorChain.interceptors 是一个 ArrayList,保持插入顺序。
代理链构建顺序
pluginAll 逐层包装
Java
// InterceptorChain.java
public Object pluginAll(Object target) {
// target 初始为原始 Executor
for (Interceptor interceptor : interceptors) {
// 顺序:A.plugin(target) → B.plugin(resultA) → C.plugin(resultB)
target = interceptor.plugin(target);
}
return target;
}
构建过程:
Java
原始 Executor
→ A.plugin(Executor) → ProxyA(target=Executor)
→ B.plugin(ProxyA) → ProxyB(target=ProxyA)
→ C.plugin(ProxyB) → ProxyC(target=ProxyB)
最终:ProxyC → ProxyB → ProxyA → 原始 Executor
先注册的插件在最内层(靠近原始对象),后注册的在最外层。
执行顺序推导
Java
调用 Executor.query()
→ ProxyC.invoke() → C.intercept() [前置C]
→ proceed()
→ ProxyB.invoke() → B.intercept() [前置B]
→ proceed()
→ ProxyA.invoke() → A.intercept() [前置A]
→ proceed()
→ 原始 Executor.query() ← 最先执行原始逻辑的是最后注册的插件内层
← A.intercept() [后置A]
← ProxyA 返回
← B.intercept() [后置B]
← ProxyB 返回
← C.intercept() [后置C]
← ProxyC 返回
结论:先注册的插件先执行前置逻辑、后执行后置逻辑(类似栈:先入后出)。
签名匹配机制
签名过滤原理
每个插件通过 @Signature 声明拦截目标,Plugin.wrap() 根据签名匹配决定是否创建代理:
XML
@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 PluginA implements Interceptor { ... }
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class PluginB implements Interceptor { ... }
PluginA仅拦截Executor,对StatementHandler返回原对象PluginB仅拦截StatementHandler,对Executor返回原对象
交叉拦截场景
Java
Executor 创建时:
A.plugin(Executor) → ProxyA ✓ (签名匹配)
B.plugin(ProxyA) → ProxyA ✗ (B 不拦截 Executor,返回原 ProxyA)
C.plugin(ProxyA) → ProxyAC ✓ (C 拦截 Executor)
最终 Executor 代理链:ProxyAC → ProxyA → 原始 Executor
StatementHandler 创建时:
A.plugin(StmtHandler) → StmtHandler ✗ (A 不拦截)
B.plugin(StmtHandler) → ProxyB ✓ (B 拦截)
C.plugin(ProxyB) → ProxyB ✗ (C 不拦截)
最终 StatementHandler 代理链:ProxyB → 原始 StatementHandler
getAllInterfaces 签名匹配
Java
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> ifc : type.getInterfaces()) {
if (signatureMap.containsKey(ifc)) { // ★ 仅在签名映射中的接口才创建代理
interfaces.add(ifc);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class[0]);
}
仅当目标对象的接口在 signatureMap 中时才创建代理,避免无效代理层。
插件叠加与依赖关系
典型插件组合
| 插件 | 拦截对象 | 拦截方法 | 职责 |
|---|---|---|---|
| 分页插件 | Executor | query | SQL 改写,添加 LIMIT |
| 性能监控 | StatementHandler | prepare/query | 计时开始/结束 |
| SQL 审计 | StatementHandler | prepare | 记录 SQL 文本 |
| 结果脱敏 | ResultSetHandler | handleResultSets | 字段解密 |
多个插件互不干扰,各自拦截不同对象。
同类插件叠加顺序问题
当多个插件拦截同一对象的同一方法时,顺序至关重要:
text
<!-- 场景:分页 + 租户隔离 + SQL 审计 -->
<plugins>
<plugin interceptor="com.example.TenantPlugin"/> <!-- 先注册 -->
<plugin interceptor="com.example.PaginationPlugin"/> <!-- 中注册 -->
<plugin interceptor="com.example.AuditPlugin"/> <!-- 后注册 -->
</plugins>
代理链结构(Executor.query):
text
ProxyAudit → ProxyPagination → ProxyTenant → 原始 Executor
执行顺序:
Audit 前置 → Pagination 前置 → Tenant 前置 → 原始 query → Tenant 后置 → Pagination 后置 → Audit 后置
分页插件必须在 Tenant 插件之前改写 SQL,否则分页的 LIMIT 可能被租户条件覆盖。正确做法:需要改写 SQL 的插件应先注册(在内层),审计类插件后注册(在外层)。
顺序控制最佳实践
text
SQL 改写类插件(分页、租户) → 先注册(内层,先执行改写)
↓
SQL 审计类插件(日志、监控) → 后注册(外层,后执行记录)
↓
结果处理类插件(脱敏、缓存) → 最后注册(最外层,处理最终结果)
插件短路场景
跳过原始执行
拦截器不调用 proceed() 实现短路:
text
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class CachePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 检查缓存
CacheKey cacheKey = createCacheKey(ms, parameter);
Object cached = localCache.get(cacheKey);
if (cached != null) {
return cached; // ★ 不调用 proceed(),短路跳过原始查询
}
// 执行原始查询
Object result = invocation.proceed();
localCache.put(cacheKey, result);
return result;
}
}
短路会跳过后续所有插件和原始方法,需谨慎使用。
多层短路的顺序影响
text
PluginA(缓存短路)→ PluginB(审计日志)→ PluginC(分页)
如果 PluginA 命中缓存短路:
→ A.intercept() 直接返回缓存结果
→ B 和 C 的拦截逻辑全部跳过
→ 审计日志不会记录,分页不会执行
正确顺序:审计应在缓存之前
PluginB(审计)→ PluginA(缓存)→ PluginC(分页)
这样即使缓存短路,审计日志也已记录
插件顺序对比表
| 配置顺序 | 代理链结构 | 前置执行顺序 | 后置执行顺序 | 适用场景 |
|---|---|---|---|---|
| A → B → C | C→B→A→原始 | C → B → A → 原始 | A → B → C → 返回 | A 先改写 SQL,C 最后审计 |
| C → B → A | A→B→C→原始 | A → B → C → 原始 | C → B → A → 返回 | A 最后审计,C 先改写 SQL |
动态插件注册(编程方式)
除 XML 配置外,可通过编程方式注册:
text
Configuration config = new Configuration();
// 动态添加插件
config.addInterceptor(new PaginationPlugin());
config.addInterceptor(new AuditPlugin());
// 在特定位置插入插件
config.getInterceptorChain().getInterceptors().add(0, new TenantPlugin());
注意:
InterceptorChain.getInterceptors()返回的是不可修改列表,运行时动态插入需通过反射或自定义扩展。
要点总结
- 插件在
mybatis-config.xml中的声明顺序决定InterceptorChain中的注册顺序 pluginAll()按注册顺序逐层包装,先注册的在内层靠近原始对象,后注册的在外层- 执行顺序:外层插件前置 → 内层插件前置 → 原始方法 → 内层插件后置 → 外层插件后置
- 签名匹配通过
@Signature的type+method+args精确匹配,不匹配的目标直接返回原对象 - SQL 改写类插件应先注册(内层),审计监控类插件后注册(外层)
- 插件不调用
proceed()实现短路,会跳过后续所有插件和原始方法 - 同类插件叠加时,顺序决定 SQL 改写的最终效果,需根据依赖关系谨慎排列
存放路径:D:\git2\jwdev\articles\MYBATIS\专家\插件开发高级应用\多插件执行顺序.md
📝 发现内容有误?点击此处直接编辑