全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

多插件执行顺序

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 中时才创建代理,避免无效代理层。

插件叠加与依赖关系

典型插件组合

插件拦截对象拦截方法职责
分页插件ExecutorquerySQL 改写,添加 LIMIT
性能监控StatementHandlerprepare/query计时开始/结束
SQL 审计StatementHandlerprepare记录 SQL 文本
结果脱敏ResultSetHandlerhandleResultSets字段解密

多个插件互不干扰,各自拦截不同对象。

同类插件叠加顺序问题

当多个插件拦截同一对象的同一方法时,顺序至关重要:

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 → CC→B→A→原始C → B → A → 原始A → B → C → 返回A 先改写 SQL,C 最后审计
C → B → AA→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() 按注册顺序逐层包装,先注册的在内层靠近原始对象,后注册的在外层
  • 执行顺序:外层插件前置 → 内层插件前置 → 原始方法 → 内层插件后置 → 外层插件后置
  • 签名匹配通过 @Signaturetype + method + args 精确匹配,不匹配的目标直接返回原对象
  • SQL 改写类插件应先注册(内层),审计监控类插件后注册(外层)
  • 插件不调用 proceed() 实现短路,会跳过后续所有插件和原始方法
  • 同类插件叠加时,顺序决定 SQL 改写的最终效果,需根据依赖关系谨慎排列

存放路径:D:\git2\jwdev\articles\MYBATIS\专家\插件开发高级应用\多插件执行顺序.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 分页插件深度定制
下一篇 → 插件代理链原理
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库