mybatis拦截器

MyBatis 拦截器(插件)深度解析:从原理到实战扩展

MyBatis 拦截器(又称插件)是 MyBatis 最强大的扩展机制,允许开发者在不修改 MyBatis 核心源码的前提下,通过动态代理对 SQL 执行流程中的关键节点进行拦截,插入自定义逻辑(如日志记录、性能监控、SQL 改写、参数加密等)。从 “核心定位→接口设计→拦截原理→实战开发” 四个维度,彻底拆解 MyBatis 拦截器的工作机制。

拦截器核心定位与拦截对象

MyBatis 拦截器的核心是 “拦截四大核心对象的方法调用”,这四大对象是 MyBatis 执行 SQL 的关键组件,覆盖 “SQL 执行→参数处理→结果映射” 全流程。

四大可拦截对象与拦截点

MyBatis 仅允许拦截以下四类核心对象的特定方法,其他对象无法通过拦截器扩展:

可拦截对象 核心作用 允许拦截的方法(示例) 典型应用场景
Executor SQL 执行器(调度 StatementHandler) query()update()commit()rollback() 全局 SQL 日志、缓存扩展、性能监控
ParameterHandler 参数处理器(绑定 SQL 参数) getParameterObject()setParameters() 参数加密 / 解密、参数校验
ResultSetHandler 结果集处理器(映射结果) handleResultSets()handleOutputParameters() 结果加密 / 解密、结果过滤、数据脱敏
StatementHandler SQL 语句处理器(创建 Statement) prepare()parameterize()update()query() SQL 改写(如动态加租户条件)、执行日志

注意:拦截器只能拦截上述对象的特定方法(需通过 @Signature 精确匹配方法名和参数列表),无法拦截对象的所有方法。

拦截器的核心价值

  • 无侵入扩展:无需修改 MyBatis 核心代码,通过配置即可接入自定义逻辑;
  • 粒度可控:可精确指定拦截的对象、方法和参数,避免全局影响;
  • 责任链模式:支持多个拦截器叠加,形成拦截链,按配置顺序执行。

拦截器核心接口与注解

MyBatis 为拦截器定义了统一的接口规范(Interceptor)和注解(@Intercepts/@Signature),开发者需遵循该规范实现自定义拦截器。

1. Interceptor 接口:拦截器的标准定义

Interceptor 是所有自定义拦截器的顶层接口,定义了三个核心方法,分别对应 “拦截逻辑”“代理生成” 和 “参数初始化”:

public interface Interceptor {
    /**
     * 1. 核心方法:拦截目标方法调用,执行自定义逻辑
     * @param invocation 封装了目标对象、目标方法、方法参数的对象
     * @return 目标方法的执行结果(可修改后返回)
     * @throws Throwable 允许抛出异常(MyBatis 会统一处理)
     */
    Object intercept(Invocation invocation) throws Throwable;

    /**
     * 2. 生成代理对象:为目标对象创建代理,将拦截器逻辑注入
     * @param target 被拦截的目标对象(如 Executor、StatementHandler)
     * @return 代理对象(默认通过 Plugin.wrap() 生成 JDK 动态代理)
     */
    default Object plugin(Object target) {
        // 调用 Plugin 工具类生成代理对象,默认实现无需修改
        return Plugin.wrap(target, this);
    }

    /**
     * 3. 初始化拦截器参数:从 MyBatis 配置中读取插件参数
     * @param properties 配置文件中为拦截器设置的参数(如 <property name="key" value="value"/>)
     */
    default void setProperties(Properties properties) {
        // 空实现,如需读取配置参数,需重写该方法
    }
}
方法职责拆解:
  • intercept(Invocation):拦截逻辑的核心,所有自定义代码(如日志、监控)都在此编写。通过 invocation.proceed() 可调用目标方法(即原 MyBatis 逻辑),支持在调用前后插入自定义逻辑;
  • plugin(Object):负责为目标对象生成代理对象,默认通过 Plugin.wrap() 实现 JDK 动态代理(无需开发者手动处理代理生成);
  • setProperties(Properties):用于初始化拦截器的配置参数(如日志开关、加密密钥),在拦截器初始化时调用一次。

2. @Intercepts 与 @Signature:拦截目标的精确匹配

仅实现 Interceptor 接口无法让 MyBatis 识别拦截器的作用范围,需通过 @Intercepts@Signature 注解明确 “拦截哪个对象的哪个方法”。

(1)@Intercepts:标识拦截器的注解

用于包裹一个或多个 @Signature,声明该拦截器的拦截目标集合(支持同时拦截多个对象的多个方法):

@Documented
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Target({ElementType.TYPE})         // 仅作用于类
public @interface Intercepts {
    // 拦截目标的签名列表(一个拦截器可拦截多个目标方法)
    Signature[] value();
}
(2)@Signature:拦截目标的精确签名

每个 @Signature 对应一个 “拦截目标”,需精确指定拦截对象类型方法名方法参数列表(三者缺一不可,否则无法匹配):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({}) // 仅作用于 @Intercepts 内部
public @interface Signature {
    // 1. 拦截的对象类型(必须是四大核心对象之一:Executor/ParameterHandler/ResultSetHandler/StatementHandler)
    Class<?> type();

    // 2. 拦截的方法名(必须与目标对象的方法名完全一致)
    String method();

    // 3. 拦截方法的参数列表(必须与目标方法的参数类型完全匹配,顺序一致)
    Class<?>[] args();
}
(3)注解使用示例

以下示例定义一个 “拦截 StatementHandler 的 prepare 方法” 的拦截器(用于 SQL 执行前的改写):

// 声明该类是拦截器,指定拦截目标
@Intercepts({
        @Signature(
                type = StatementHandler.class,  // 拦截 StatementHandler 对象
                method = "prepare",             // 拦截 prepare 方法
                args = {Connection.class, Integer.class} // prepare 方法的参数列表(必须精确匹配)
        )
})
public class MyStatementHandlerInterceptor implements Interceptor {
    // 实现 Interceptor 接口方法...
}

关键提醒:args 参数必须与目标方法的参数类型完全一致(包括顺序)。例如 StatementHandler.prepare() 的源码为 Statement prepare(Connection connection, Integer transactionTimeout),因此 args 必须是 {Connection.class, Integer.class},否则无法匹配。

拦截器的底层实现原理

MyBatis 拦截器基于 JDK 动态代理责任链模式 实现,核心流程是 “创建核心对象时生成代理→方法调用时触发拦截链→执行自定义逻辑并调用原方法”。

1. 核心机制:动态代理与 Plugin 类

MyBatis 通过 Plugin 类(实现 InvocationHandler 接口)生成目标对象的代理,Plugin.wrap() 是代理生成的入口:

Plugin 类核心源码
public class Plugin implements InvocationHandler {
    // 被代理的目标对象(如 StatementHandler)
    private final Object target;
    // 拦截器实例(自定义的 Interceptor)
    private final Interceptor interceptor;
    // 拦截器要拦截的方法映射(key:目标对象类型,value:该类型下可拦截的方法集合)
    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;
    }

    /**
     * 静态方法:为目标对象生成代理(拦截器的 plugin 方法默认调用此方法)
     * @param target 目标对象
     * @param interceptor 拦截器
     * @return 代理对象(若目标对象无需拦截,返回原对象)
     */
    public static Object wrap(Object target, Interceptor interceptor) {
        // 1. 解析拦截器的 @Intercepts 注解,获取要拦截的方法映射
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 2. 获取目标对象的所有接口(JDK 动态代理需基于接口)
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        // 3. 若有可拦截的接口,生成代理对象;否则返回原对象
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(
                    type.getClassLoader(),  // 类加载器
                    interfaces,             // 目标对象实现的接口
                    new Plugin(target, interceptor, signatureMap) // InvocationHandler
            );
        }
        return target;
    }

    /**
     * 代理对象的核心方法:拦截目标方法调用
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 1. 查找当前方法是否在拦截范围内(根据 signatureMap)
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            if (methods != null && methods.contains(method)) {
                // 2. 若在拦截范围内,调用拦截器的 intercept 方法(执行自定义逻辑)
                return interceptor.intercept(new Invocation(target, method, args));
            }
            // 3. 若不在拦截范围内,直接调用目标方法(原 MyBatis 逻辑)
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }

    // 解析 @Intercepts 注解,生成“目标类型→可拦截方法”的映射
    private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
        if (interceptsAnnotation == null) {
            throw new PluginException("拦截器必须添加 @Intercepts 注解");
        }
        Signature[] sigs = interceptsAnnotation.value();
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
            Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
            try {
                // 根据签名匹配目标方法(精确匹配方法名和参数列表)
                Method method = sig.type().getMethod(sig.method(), sig.args());
                methods.add(method);
            } catch (NoSuchMethodException e) {
                throw new PluginException("拦截方法不存在:" + sig.type() + "." + sig.method(), e);
            }
        }
        return signatureMap;
    }

    // 获取目标对象实现的、且在拦截范围内的所有接口
    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]);
    }
}
代理生成流程:
  1. 解析注解getSignatureMap() 解析拦截器的 @Intercepts 注解,生成 “目标对象类型→可拦截方法” 的映射;
  2. 匹配接口getAllInterfaces() 筛选目标对象实现的、且在拦截范围内的接口(JDK 动态代理需基于接口);
  3. 生成代理:若存在可拦截接口,通过 Proxy.newProxyInstance() 生成代理对象,否则返回原对象。

2. 拦截链生成:InterceptorChain 责任链

MyBatis 通过 InterceptorChain(拦截器链)管理所有注册的拦截器,在创建四大核心对象时,依次为对象生成代理,形成多层代理链

(1)InterceptorChain 核心源码
public class InterceptorChain {
    // 存储所有注册的拦截器(按配置顺序)
    private final List<Interceptor> interceptors = new ArrayList<>();

    // 为目标对象执行所有拦截器的 plugin 方法,生成代理链
    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            // 每个拦截器依次为目标对象生成代理(代理嵌套代理)
            target = interceptor.plugin(target);
        }
        return target;
    }

    // 添加拦截器(MyBatis 初始化时从配置中加载)
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    // 获取所有拦截器
    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }
}
(2)拦截链融入 MyBatis 初始化流程

MyBatis 在 Configuration 类中创建四大核心对象时,会调用 interceptorChain.pluginAll() 为对象生成代理链。例如:

// Configuration 类:创建 StatementHandler 并生成代理链
public StatementHandler newStatementHandler(Executor executor, MappedStatement ms, 
                                           Object parameterObject, RowBounds rowBounds, 
                                           ResultHandler resultHandler, BoundSql boundSql) {
    // 1. 创建原始 StatementHandler 对象(RoutingStatementHandler)
    StatementHandler statementHandler = new RoutingStatementHandler(
        executor, ms, parameterObject, rowBounds, resultHandler, boundSql
    );
    // 2. 调用拦截器链,为原始对象生成代理(多层代理)
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

// 同理,创建 Executor、ParameterHandler、ResultSetHandler 时也会调用 pluginAll()
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // 1. 创建原始 Executor 对象
    Executor executor = ...; 
    // 2. 生成代理链
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
(3)拦截链执行顺序

假设配置了两个拦截器:InterceptorA(拦截 StatementHandler.prepare ())和 InterceptorB(拦截 StatementHandler.prepare ()),配置顺序为 A→B,则代理链生成和执行顺序如下:

  1. 代理生成:原始对象 → A 的代理 → B 的代理(最终返回 B 的代理);
  2. 方法调用Binvoke()Bintercept()Ainvoke()Aintercept() → 原始对象的 prepare()
  3. 结果返回:原始结果 → A 的处理结果 → B 的处理结果 → 最终返回给调用者。

结论:拦截器的配置顺序决定代理链的嵌套顺序,配置在前的拦截器会被后配置的拦截器包裹,执行时后配置的拦截器先触发。

3. Invocation 类:封装拦截上下文

Invocation 类封装了 “目标对象、目标方法、方法参数”,是拦截器与目标方法之间的桥梁,通过 invocation.proceed() 可调用目标方法(或下一层代理):

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 proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }

    // Getter 方法(获取目标对象、方法、参数)
    public Object getTarget() { return target; }
    public Method getMethod() { return method; }
    public Object[] getArgs() { return args; }
}

intercept() 方法中,通过 invocation.proceed() 触发后续逻辑,示例:

@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 1. 执行自定义前置逻辑(如记录开始时间)
    long start = System.currentTimeMillis();
    try {
        // 2. 调用下一层逻辑(下一个拦截器或原始方法)
        return invocation.proceed();
    } finally {
        // 3. 执行自定义后置逻辑(如计算执行耗时)
        long cost = System.currentTimeMillis() - start;
        System.out.println("SQL 执行耗时:" + cost + "ms");
    }
}

实战:自定义 SQL 执行时间监控拦截器

通过一个完整示例,演示如何开发、配置和使用自定义拦截器,实现 “监控所有 SQL 的执行时间” 功能。

1. 步骤 1:实现自定义拦截器

拦截 Executor.query()Executor.update() 方法(覆盖查询和增删改操作),统计执行耗时:

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;

// 1. 用 @Intercepts 和 @Signature 声明拦截目标
@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 SqlExecuteTimeInterceptor implements Interceptor {
    // 2. 初始化拦截器参数(从配置文件读取,可选)
    @Override
    public void setProperties(Properties properties) {
        // 示例:读取配置的日志开关(如 <property name="logEnable" value="true"/>)
        String logEnable = properties.getProperty("logEnable", "true");
        System.out.println("SQL 耗时监控日志开关:" + logEnable);
    }

    // 3. 生成代理对象(默认使用 Plugin.wrap(),无需修改)
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 4. 核心拦截逻辑:统计 SQL 执行时间
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 4.1 前置逻辑:获取 SQL 标识和开始时间
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        String sqlId = ms.getId(); // SQL 标识(如 com.example.UserMapper.selectUserById)
        String sqlCommandType = ms.getSqlCommandType().name(); // SQL 类型(SELECT/INSERT/UPDATE/DELETE)
        long startTime = System.currentTimeMillis();

        try {
            // 4.2 调用目标方法(执行 SQL)
            return invocation.proceed();
        } finally {
            // 4.3 后置逻辑:计算耗时并打印日志
            long costTime = System.currentTimeMillis() - startTime;
            System.out.printf("[SQL 监控] SQL_ID: %s, 类型: %s, 执行耗时: %d ms%n",
                    sqlId, sqlCommandType, costTime);
        }
    }
}

2. 步骤 2:配置拦截器(mybatis-config.xml)

在 MyBatis 全局配置文件中注册拦截器,并可设置自定义参数:

<configuration>
    <!-- 配置拦截器链 -->
    <plugins>
        <plugin interceptor="com.example.interceptor.SqlExecuteTimeInterceptor">
            <!-- 可选:为拦截器设置参数(通过 setProperties 方法注入) -->
            <property name="logEnable" value="true"/>
        </plugin>
    </plugins>

    <!-- 其他配置(如数据源、mappers)省略 -->
</configuration>

3. 步骤 3:测试拦截效果

调用任意 Mapper 方法(如 userMapper.selectUserById(1)),控制台会输出类似日志:

SQL 耗时监控日志开关:true
[SQL 监控] SQL_ID: com.example.mapper.UserMapper.selectUserById, 类型: SELECT, 执行耗时: 15 ms
[SQL 监控] SQL_ID: com.example.mapper.UserMapper.insertUser, 类型: INSERT, 执行耗时: 8 ms

开发拦截器的关键工具:MetaObject

在拦截器中,经常需要读取或修改 MyBatis 核心对象的属性(如 StatementHandlerBoundSqlExecutorTransaction),但这些对象的属性多为 private,无法直接访问。MyBatis 提供 MetaObject 工具类,通过反射安全地操作对象属性,支持 OGNL 表达式。

1. MetaObject 核心功能

// 1. 创建 MetaObject 实例(传入要操作的对象和 MyBatis 配置)
MetaObject metaObject = SystemMetaObject.forObject(target, configuration.getObjectFactory(), 
                                                  configuration.getObjectWrapperFactory(), 
                                                  configuration.getReflectorFactory());

// 2. 读取属性值(支持 OGNL 表达式,如 "boundSql.sql" 读取嵌套属性)
String sql = (String) metaObject.getValue("boundSql.sql"); // 读取 StatementHandler 的 SQL 语句
Object parameter = metaObject.getValue("parameterHandler.parameterObject"); // 读取参数对象

// 3. 修改属性值(支持 OGNL 表达式)
metaObject.setValue("boundSql.sql", modifiedSql); // 修改 SQL 语句(如动态添加租户条件)

2. 实战示例:用 MetaObject 改写 SQL

StatementHandler.prepare() 拦截器中,通过 MetaObject 读取并修改 SQL,为所有查询添加租户 ID 条件(多租户场景):

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
public class TenantSqlInterceptor implements Interceptor {
    private String tenantId = "1001"; // 租户 ID(实际可从上下文获取)

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取被代理的 StatementHandler 对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 2. 创建 MetaObject,用于操作 StatementHandler 的属性
        MetaObject metaObject = SystemMetaObject.forObject(
                statementHandler,
                new DefaultObjectFactory(),
                new DefaultObjectWrapperFactory(),
                new DefaultReflectorFactory()
        );

        // 3. 读取 BoundSql(封装 SQL 的对象)
        BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql");
        String originalSql = boundSql.getSql(); // 原始 SQL

        // 4. 改写 SQL:为 SELECT 语句添加租户条件(示例逻辑)
        if (originalSql.trim().toUpperCase().startsWith("SELECT")) {
            String modifiedSql = originalSql + " WHERE tenant_id = '" + tenantId + "'";
            // 5. 修改 BoundSql 的 sql 属性(通过 MetaObject 反射修改 private 属性)
            metaObject.setValue("boundSql.sql", modifiedSql);
            System.out.println("改写后 SQL:" + modifiedSql);
        }

        // 6. 执行目标方法
        return invocation.proceed();
    }

    // plugin 和 setProperties 方法省略...
}

拦截器开发注意事项

  1. 精确匹配方法签名@Signaturemethodargs 必须与目标方法完全一致(如 Executor.query() 有多个重载,需明确参数列表),否则拦截失效;
  2. 避免循环代理:不要拦截 Plugin 类或自身,否则会导致无限循环;
  3. 性能影响:拦截器会增加方法调用开销,避免在拦截逻辑中执行耗时操作(如 IO 操作);
  4. 线程安全:拦截器是单例对象,setProperties 初始化的参数需保证线程安全(避免使用非线程安全的成员变量);
  5. 优先使用 MetaObject:操作 MyBatis 核心对象的属性时,必须通过 MetaObject,避免直接反射(MyBatis 版本升级可能导致属性名变化)。

总结

MyBatis 拦截器是 MyBatis 灵活性的核心体现,其底层基于 JDK 动态代理和责任链模式,允许开发者在 SQL 执行流程中插入自定义逻辑。关键要点:

  1. 拦截范围:仅支持拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四大核心对象的特定方法;
  2. 开发规范:需实现 Interceptor 接口,并通过 @Intercepts/@Signature 声明拦截目标;
  3. 核心工具Plugin 生成代理,Invocation 封装拦截上下文,MetaObject 操作核心对象属性;
  4. 实战场景:日志记录、性能监控、SQL 改写、参数 / 结果加密等