MyBatis Interceptor执行顺序详解(plugin机制、责任链模式)

目录

一、引言

在MyBatis中,Interceptor(拦截器)是实现自定义功能的核心扩展点。通过插件机制,开发者可以在SQL执行、参数处理、结果映射等关键流程中插入自定义逻辑。本文将深入介绍MyBatis中Interceptor的执行顺序,重点关注Interceptor的注册顺序、plugin机制、Configuration如何调用pluginAll方法生成Executor,以及责任链模式的应用,并详细说明了"先注册后执行"的机制和Plugin.wrap底层实现。


二、Interceptor的注册顺序

MyBatis的Interceptor通常通过配置文件或代码注册到Configuration对象中。注册顺序非常重要,因为它直接影响拦截器的执行顺序。

2.1 配置文件注册(mybatis-config.xml)

xml 复制代码
<plugins>
  <plugin interceptor="com.example.MyInterceptor1"/>
  <plugin interceptor="com.example.MyInterceptor2"/>
</plugins>

拦截器会按照配置文件中出现的顺序依次注册。

2.2 代码注册

java 复制代码
configuration.addInterceptor(new MyInterceptor1());
configuration.addInterceptor(new MyInterceptor2());

同样,拦截器会按照addInterceptor的调用顺序注册。

2.3 SpringBoot @Order

如果你使用的是 mybatis-spring-boot-starter(推荐方式),可以通过 @Order 注解明确指定顺序:

java 复制代码
@Bean
@Order(1)
public Interceptor interceptorA() { ... }

@Bean
@Order(2)
public Interceptor interceptorB() { ... }

注册顺序说明:

  • Spring 会将所有类型为 Interceptor 的 Bean 注入到 MyBatis 的 SqlSessionFactory 中。
  • 默认情况下,Spring 注入集合类型(如 List)时,顺序为 Bean上@Order注解声明的顺序。

2.4 扩展 - PageHelper链最后(即最先执行)

PageHelper通过实现InitializingBean.afterPropertiesSet方法,强制将PageInterceptor放到拦截器列表的最后面,即保证了PageInterceptor会最先被执行,具体代码如下:

java 复制代码
package com.github.pagehelper.autoconfigure;

import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import java.util.List;

/**
 * 自定注入分页插件
 *
 * @author liuzh
 */
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties({PageHelperProperties.class, PageHelperStandardProperties.class})
@AutoConfigureAfter(MybatisAutoConfiguration.class)
//@Import(PageHelperProperties.class)
@Lazy(false)
public class PageHelperAutoConfiguration implements InitializingBean {

    private final List<SqlSessionFactory> sqlSessionFactoryList;

    private final PageHelperProperties properties;

    public PageHelperAutoConfiguration(List<SqlSessionFactory> sqlSessionFactoryList, PageHelperStandardProperties standardProperties) {
        this.sqlSessionFactoryList = sqlSessionFactoryList;
        this.properties = standardProperties.getProperties();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        PageInterceptor interceptor = new PageInterceptor();
        interceptor.setProperties(this.properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
            if (!containsInterceptor(configuration, interceptor)) {
                //========== 重点关注 =============
                configuration.addInterceptor(interceptor);
            }
        }
    }

    /**
     * 是否已经存在相同的拦截器
     *
     * @param configuration
     * @param interceptor
     * @return
     */
    private boolean containsInterceptor(org.apache.ibatis.session.Configuration configuration, Interceptor interceptor) {
        try {
            // getInterceptors since 3.2.2
            return configuration.getInterceptors().stream().anyMatch(config->interceptor.getClass().isAssignableFrom(config.getClass()));
        } catch (Exception e) {
            return false;
        }
    }

}

三、plugin机制与InterceptorChain

3.1 InterceptorChain.pluginAll

MyBatis通过plugin机制实现拦截器的动态包装。核心类是InterceptorChain,它维护一个Interceptor列表,并负责将目标对象(如Executor、StatementHandler等)按顺序包装。

InterceptorChain源码片段

java 复制代码
public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<>();

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            target = interceptor.plugin(target);
        }
        return target;
    }
}

可以看到,pluginAll方法会按照Interceptor注册顺序依次调用每个Interceptor的plugin方法,将目标对象层层包装。

3.2 Interceptor.plugin

Interceptor接口

java 复制代码
public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    default void setProperties(Properties properties) {}
}

3.3 Plugin.wrap(target, this)

MyBatis的plugin机制核心在于Plugin.wrap(target, this)方法。其底层实现主要依赖Java的动态代理(JDK Proxy),实现了对目标对象的拦截。

主要流程

  1. 判断目标对象是否需要拦截

    Plugin.wrap会根据Interceptor的@Signature注解,判断目标对象是否实现了需要拦截的接口(如Executor、StatementHandler等)。

  2. 创建代理对象

    如果需要拦截,Plugin.wrap会使用JDK动态代理生成一个代理对象。代理对象会在目标方法被调用时,自动调用Interceptor的intercept方法。

  3. 责任链传递

    多个Interceptor包装后,代理对象会层层嵌套,形成责任链。每个intercept方法内部通常会调用invocation.proceed(),继续传递到下一个Interceptor或原始对象。

源码简要

java 复制代码
public class Plugin implements InvocationHandler {
    private Object target;
    private Interceptor interceptor;
    private Map<Class<?>, Set<Method>> signatureMap;

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // 判断是否需要拦截
        if (shouldIntercept(type, signatureMap)) {
            return Proxy.newProxyInstance(
                type.getClassLoader(),
                getInterfaces(type, signatureMap),
                new Plugin(target, interceptor, signatureMap)
            );
        }
        return target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 判断是否拦截该方法
        if (shouldIntercept(method)) {
            return interceptor.intercept(new Invocation(target, method, args));
        }
        return method.invoke(target, args);
    }
}

四、Configuration调用pluginAll生成Executor

在MyBatis初始化时,Configuration会创建Executor等核心组件,并通过InterceptorChain的pluginAll方法进行包装:

典型流程(以Executor为例)

java 复制代码
Executor executor = new SimpleExecutor();
executor = interceptorChain.pluginAll(executor);

这意味着Executor会被所有Interceptor按注册顺序依次包装,形成一条"责任链"。


五、责任链模式的应用

MyBatis的plugin机制本质上是责任链模式(Chain of Responsibility Pattern)的应用。每个Interceptor都可以选择拦截或放行操作,最终形成如下调用链:

  1. Executor被第一个Interceptor包装,返回一个代理对象。
  2. 代理对象再被第二个Interceptor包装,依次类推。
  3. 执行SQL时,调用链会从最外层Interceptor开始,逐层向内传递,直到原始Executor。

这种模式带来的好处是:拦截器可以灵活地插入、组合,且执行顺序完全由注册顺序决定。


六、执行顺序重点:先注册后执行

需要特别强调的是:Interceptor的执行顺序遵循"先注册,后执行"原则。

  • 注册时,Configuration会将Interceptor按顺序添加到InterceptorChain的interceptors列表。
  • 包装时,pluginAll方法会依次调用每个Interceptor的plugin方法,将目标对象层层包裹。
  • 执行时,调用链会从最外层(最后注册的Interceptor)开始,逐层向内传递,直到最先注册的Interceptor,最后到原始对象。

举例说明:

假设注册顺序为:InterceptorA → InterceptorB → InterceptorC

执行包装的顺序(InterceptorChain.pluginAll):InterceptorA.plugin → InterceptorB.plugin → InterceptorC.plugin

最终目标对象的包装顺序为:C包裹B,B包裹A,A包裹原始对象

执行顺序为:C → B → A → 原始对象


七、示例代码

假设有两个拦截器:

java 复制代码
public class MyInterceptor1 implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyInterceptor1 before");
        Object result = invocation.proceed();
        System.out.println("MyInterceptor1 after");
        return result;
    }
}

public class MyInterceptor2 implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("MyInterceptor2 before");
        Object result = invocation.proceed();
        System.out.println("MyInterceptor2 after");
        return result;
    }
}

注册顺序为1→2,执行顺序如下:

复制代码
MyInterceptor2 before
MyInterceptor1 before
[原始Executor执行]
MyInterceptor1 after
MyInterceptor2 after

八、结论

  • Interceptor注册顺序决定执行顺序:先注册的拦截器会被后执行,责任链模式保证了拦截器的顺序性和可扩展性。
  • Plugin.wrap(target, this):通过JDK动态代理实现拦截器包装,只有目标对象实现了需要拦截的接口才会被代理,拦截器通过intercept方法实现自定义逻辑。
  • 实际开发中,务必关注拦截器的注册顺序,合理设计拦截器的职责和顺序,确保业务逻辑的正确性和可维护性。

如需进一步了解,可以参考MyBatis源码中的InterceptorChainPlugin类实现,以及项目中的拦截器注册代码。

相关推荐
weixin_704266053 小时前
Spring整合MyBatis(一)
java·spring·mybatis
寻见9033 小时前
10 分钟吃透 MyBatis 核心|从底层原理到实战技巧,Java 开发者必藏(无废话干货)
java·mysql·mybatis
TTc_6 小时前
对于子查询语句多条sql报错排查
数据库·sql·mybatis
小涛不学习7 小时前
MyBatis-Plus 完整教程(入门到实战)
mybatis
一只小bit8 小时前
JavaWeb 开发 —— 从 JDBC 到 Mybatis 数据库使用
数据库·maven·mybatis
云澜哥哥8 小时前
MyBatis 实战指南:特殊符号处理与高效批量操作
java·jvm·mybatis
小箌11 小时前
JavaWeb_02
java·数据库·maven·mybatis
wsxlgg1 天前
mybatis自动生成mapper和xml文件
mybatis
杰克尼1 天前
苍穹外卖--day11
java·数据库·spring boot·mybatis·notepad++