MyBatis 拦截器(Interceptor)

概览

MyBatis 拦截器(又称插件)是其核心扩展机制,通过jdk动态代理(JDK Proxy)对 MyBatis 核心执行流程(SQL 执行、参数处理、结果集转换等)进行拦截,注入自定义逻辑。其核心是 org.apache.ibatis.plugin.Interceptor 接口,配合 @Intercepts/@Signature 注解声明拦截规则,Invocation 封装调用上下文,Plugin 实现动态代理逻辑。

1. 使用场景

分页(可参考mybatis plus)

公共字段统一赋值

性能监控打印sql日志

权限等

2. mybatis 插件可拦截的方法

  1. 执行器( Executor(org.apache.ibatis.executor.Executor) )
  2. sql语法构建器 StatementHandler
  3. 参数处理器ParameterHandler和结果处理器ResultSetHandler

3.核心接口 注解 功能详解

Interceptor 接口

源码

package org.apache.ibatis.plugin;

import java.util.Properties;

/**
 * @author Clinton Begin
 */
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable; //核心方法

  // 默认方通过 Plugin.wrap 方法指定代理的对象和当前拦截器完成插件注入 (通过代理实现)
  default Object plugin(Object target) { 
    return Plugin.wrap(target, this);
  }

  // 参数配置 (可以通过配置文件设置诸多功能,如是否启用等)
  default void setProperties(Properties properties) {
    // NOP
  }

}

其中

Object intercept(Invocation invocation) throws Throwable;

方法 是核心方法, 用来真正执行拦截的操作

Invocation

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;

/**
 * @author Clinton Begin
 */
public class Invocation {

  private static final List<Class<?>> targetClasses = Arrays.asList(Executor.class, ParameterHandler.class,
      ResultSetHandler.class, StatementHandler.class);
  private final Object target; // 代理的对象
  private final Method method; // 执行的方法
  private final Object[] args; // 参数信息(基于参数信息值定位到具体执行的方法)

  public Invocation(Object target, Method method, Object[] args) {
    if (!targetClasses.contains(method.getDeclaringClass())) {
      throw new IllegalArgumentException("Method '" + method + "' is not supported as a plugin target.");
    }
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

相关注解

@Intercepts (org.apache.ibatis.plugin.Intercepts)

@Intercepts的用法是配合Interceptor接口,定义拦截器类。例如代码中的示例:

@Intercepts({ 
  @Signature(
    type = Executor.class,  // 拦截Executor(执行器)接口
    method = "update",      // 指定Executor(执行器)中的update方法
    args = { MappedStatement.class, Object.class }  // 通过参数类型指定具体的 Executor 中的 update方法
  ) 
})
public class ExamplePlugin implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    // 执行拦截前的逻辑(如记录开始时间)
    Object result = invocation.proceed(); // 执行原方法
    // 执行拦截后的逻辑(如记录结束时间、计算耗时)
    return result;
  }
}

##4. 拦截器开发完整步骤

自动填充时间拦截器为例,演示从需求到落地的完整流程:

步骤 1:需求分析

  • 拦截 Executorupdate 方法(覆盖所有 SQL 执行)
  • 反射获取实体中的 createdAt / updatedAt 字段
  • 反射设置字段值,在SQL执行前自动填充时间字段

步骤 2:定义拦截器类(加注解+实现接口)

package com.datangyun.pcc.base.common.config;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * MyBatis拦截器:
 * 在执行 INSERT / UPDATE SQL 之前,自动填充实体的 createdAt / updatedAt 字段
 */
@Intercepts(@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}))
public class AutoFillTimeInterceptor implements Interceptor {

    // 字段缓存,避免重复反射
    private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();

    /**
     * 拦截方法,在SQL执行前自动填充时间字段
     *
     * @param invocation 方法调用信息,包含SQL语句和参数等上下文信息
     * @return 返回原始方法执行的结果
     * @throws Throwable 当方法执行出现异常时抛出
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取映射的SQL语句和参数
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];

        if (parameter != null) {
            Date now = new Date();
            SqlCommandType sqlType = ms.getSqlCommandType();

            // 根据参数类型分别处理时间字段填充
            if (parameter instanceof Collection<?> collection) {
                // 如果参数是集合类型,遍历集合并为每个对象填充时间
                for (Object obj : collection) {
                    fillTime(obj, sqlType, now);
                }
            } else {
                // 如果参数是单个对象,直接填充时间
                fillTime(parameter, sqlType, now);
            }
        }
        // 继续执行原始方法
        return invocation.proceed();
    }


    /**
     * 填充时间戳
     */
    private void fillTime(Object parameter, SqlCommandType sqlType, Date now) {
        Class<?> clazz = parameter.getClass();
        Map<String, Field> fields = FIELD_CACHE.computeIfAbsent(clazz, this::resolveFields);

        switch (sqlType) {
            case INSERT -> {
                setFieldIfPresent(parameter, fields.get("createdAt"), now, true);
                setFieldIfPresent(parameter, fields.get("updatedAt"), now, true);
            }
            case UPDATE -> {
                setFieldIfPresent(parameter, fields.get("updatedAt"), now, false);
            }
            default -> { /* 其它 SQL 类型忽略 */ }
        }
    }

    /**
     * 反射获取实体中的 createdAt / updatedAt 字段,并缓存
     */
    private Map<String, Field> resolveFields(Class<?> clazz) {
        Map<String, Field> fieldMap = new HashMap<>(2);
        try {
            Field created = clazz.getDeclaredField("createdAt");
            created.setAccessible(true);
            fieldMap.put("createdAt", created);
        } catch (NoSuchFieldException ignored) {
            // 忽略异常,若实体类没有 createdAt 字段,则不填充 createdAt 字段
        }
        try {
            Field updated = clazz.getDeclaredField("updatedAt");
            updated.setAccessible(true);
            fieldMap.put("updatedAt", updated);
        } catch (NoSuchFieldException ignored) {
            // 忽略异常,若实体类没有 updatedAt 字段,则不填充 updatedAt 字段
        }
        return fieldMap;
    }

    /**
     * 反射设置字段值
     *
     * @param obj        实体对象
     * @param field      字段(可能为 null)
     * @param value      要设置的值
     * @param onlyIfNull 是否仅在字段为 null 时才设置
     */
    private void setFieldIfPresent(Object obj, Field field, Object value, boolean onlyIfNull) {
        if (field == null) return;
        try {
            Object currentValue = field.get(obj);
            if (!onlyIfNull || currentValue == null) {
                field.set(obj, value);
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException("自动填充时间戳失败: " + e.getMessage(), e);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可扩展配置
    }
}

步骤 3:配置拦截器

根据项目环境(传统 XML 或 Spring Boot)选择配置方式:

方式 1:传统 MyBatis XML 配置(mybatis-config.xml

<configuration>
    <plugins>
        <!-- 配置自动填充时间拦截器 -->
        <plugin interceptor="com.datangyun.pcc.base.common.config.AutoFillTimeInterceptor">
        </plugin>
    </plugins>
</configuration>

方式 2:Spring Boot 配置(推荐)

Spring Boot 中无需 XML,可通过 Configuration 类注册拦截器:

package com.datangyun.pcc.base.common.config;


import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import org.apache.ibatis.logging.slf4j.Slf4jImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisConfig {

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            configuration.setLogImpl(Slf4jImpl.class);
            configuration.addInterceptor(new SqlInterceptor()); // 打印sql日志拦截器
            configuration.addInterceptor(new AutoFillTimeInterceptor()); // 自动填充时间拦截器
        };
    }
}

方式 3:Spring Boot 中通过 @Component 注册(更简单)

直接在拦截器类上添加 @Component

附sql日志拦截器

SqlInterceptor

package com.datangyun.pcc.base.common.config;

import com.github.vertical_blank.sqlformatter.SqlFormatter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.ObjectUtils;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;

/**
 * @author 冰白寒祭
 * @date 2025-09-29
 * @description //添加一个自定义的SQL拦截器插件,用来打印sql日志
 * <p>
 */

@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
@Slf4j
public class SqlInterceptor implements Interceptor {


    /**
     * 生成最终的SQL语句
     * 该方法从MyBatis的Invocation对象中提取MappedStatement和参数信息,
     * 然后根据参数映射替换SQL中的占位符,最终返回完整的SQL语句。
     * @param invocation MyBatis的Invocation对象,封装了执行SQL的相关信息
     * @return 替换参数后的完整SQL语句
     */
    private static String generateSql(Invocation invocation) {
        // 获取到BoundSql以及Configuration对象
        // BoundSql 对象存储了一条具体的 SQL 语句及其相关参数信息。
        // Configuration 对象保存了 MyBatis 框架运行时所有的配置信息
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        // Configuration保存了MyBatis运行时所有配置信息
        Configuration configuration = statement.getConfiguration();
        // BoundSql存储了具体的SQL语句 及其相关参数信息
        BoundSql boundSql = statement.getBoundSql(parameter);

        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 获取参数映射
        List<ParameterMapping> params = boundSql.getParameterMappings();
        // 获取到执行的SQL
        String sql = boundSql.getSql();
        // SQL中多个空格使用一个空格代替
        sql = sql.replaceAll("\\s+", " ");
        if (!ObjectUtils.isEmpty(params) && !ObjectUtils.isEmpty(parameterObject)) {
            // TypeHandlerRegistry 是 MyBatis 用来管理 TypeHandler 的注册器。TypeHandler 用于在 Java 类型和 JDBC 类型之间进行转换
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            // 如果参数对象的类型有对应的 TypeHandler,则使用 TypeHandler 进行处理
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject)));
            } else {
                // 否则,逐个处理参数映射
                for (ParameterMapping param : params) {
                    // 获取参数的属性名
                    String propertyName = param.getProperty();
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    // 检查对象中是否存在该属性的 getter 方法,如果存在就取出来进行替换
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                        // 检查 BoundSql 对象中是否存在附加参数。附加参数可能是在动态 SQL 处理中生成的,有的话就进行替换
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                    } else {
                        // 如果都没有,说明SQL匹配不上,带上“缺失”方便找问题
                        sql = sql.replaceFirst("\\?", "缺失");
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 获取参数值的字符串表示形式
     *
     * @param object 需要转换为字符串的参数对象
     * @return 返回参数值的字符串表示,字符串类型会添加单引号包围,日期类型会格式化后添加单引号包围,其他非空对象直接调用toString方法
     */
    private static String getParameterValue(Object object) {
        String value = "";
        // 根据对象类型进行不同的处理
        if (object instanceof String) {
            value = "'" + object + "'";
        } else if (object instanceof Date) {
            DateFormat format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + format.format((Date) object) + "'";
        } else if (!ObjectUtils.isEmpty(object)) {
            value = object.toString();
        }
        return value;
    }


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 计算这一次SQL执行前后的时间,统计一下执行耗时
        long startTime = System.currentTimeMillis();
        Object proceed = invocation.proceed();
        long endTime = System.currentTimeMillis();

        String printSql = null;
        try {
            // 通过generateSql方法拿到最终生成的SQL
            printSql = generateSql(invocation);
        } catch (Exception exception) {
            log.error("获取sql异常", exception);
        } finally {
            // 拼接日志打印过程
            long costTime = endTime - startTime; //  确保即使异常也能统计
            String formattedSql = SqlFormatter.format(printSql) + ";";

            log.info("""
                    
                    =============== Sql Logger ===============
                    {}
                    
                    执行线程: [{}]
                    执行SQL耗时: [{}ms]
                    ===========================================
                    """, formattedSql, Thread.currentThread().getName(), costTime);

        }
        return proceed;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以通过properties配置插件参数
    }
}

5. 常见拦截点与典型场景

不同拦截点对应 MyBatis 不同执行阶段,需根据需求选择合适的拦截点:

拦截点(接口)拦截时机典型场景常用 API 操作
ExecutorSQL 执行前/后(查询、增删改)统计执行时间、缓存控制、动态数据源路由getSqlCommandType()getTransaction()proceed()
StatementHandlerStatement 创建/准备前/后修改 SQL(多租户、分表)、记录原始 SQLgetBoundSql()setSql()prepare()
ParameterHandler参数设置到 Statement 前参数加密/解密、动态添加参数(如逻辑删除)setParameters()getParameterObject()
ResultSetHandler结果集转换为对象前/后结果脱敏、数据格式转换(如 Date→String)handleResultSets()getResultMaps()

6. 进阶注意事项与踩坑指南

  1. 拦截器单例与线程安全
    拦截器是单例对象,实例变量必须是线程安全的(如 final 修饰的配置参数、ThreadLocal),禁止定义可变状态(如 private int count,会导致多线程竞争)。

  2. 避免拦截代理对象
    拦截 StatementHandlerParameterHandler 时,目标对象可能是代理(如分页插件生成的代理),直接操作会导致异常。需通过 MetaObject 获取原始对象:

    // 正确获取原始 StatementHandler
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    StatementHandler originalHandler = (StatementHandler) metaObject.getValue("h.target");
    
  3. 修改 SQL 时注意参数对应
    若通过 BoundSql.setSql() 修改 SQL(如添加条件),需确保参数个数与 SQL 占位符匹配,避免 SQLSyntaxErrorExceptionParameterIndexOutOfBoundsException

  4. Spring 依赖注入问题
    若拦截器需依赖 Spring Bean(如 LogService),需确保拦截器由 Spring 管理(如添加 @Component),避免手动 new 导致依赖注入失败。

  5. 拦截顺序影响结果
    多个拦截器按配置顺序形成代理链,先配置的拦截器先执行前置逻辑,后执行后置逻辑。例如:
    配置顺序:拦截器 A → 拦截器 B
    执行顺序:A 前置 → B 前置 → 原始方法 → B 后置 → A 后置。

7. 调试技巧进阶

  1. 打印代理链结构
    plugin 方法中打印目标对象类型,确认是否被代理:

    @Override
    public Object plugin(Object target) {
        logger.debug("目标对象类型:{},是否代理:{}", 
                target.getClass().getName(), 
                Proxy.isProxyClass(target.getClass()));
        return Plugin.wrap(target, this);
    }
    
  2. 查看最终执行的 SQL(含参数)
    拦截 StatementHandler.prepare 后,通过 PreparedStatementtoString() 查看参数填充后的 SQL(部分数据库驱动支持,如 MySQL):

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        logger.debug("最终执行 SQL:{}", ps.toString()); // 输出:com.mysql.cj.jdbc.ClientPreparedStatement: SELECT * FROM user WHERE id = 1
        return ps;
    }
    
  3. 断点调试代理逻辑
    intercept 方法中打断点,通过 IDE 的“查看变量”功能分析 Invocationtargetmethodargs,定位问题。

8. 常见问题(FAQ)补充

Q1:拦截器在 Spring Boot 中不生效?

  • 检查拦截器是否注册:通过 ConfigurationCustomizer@Component 注册;
  • 检查 @Signature 注解:args 类型需与目标方法完全匹配(如 RowBounds 不能写成 int);
  • 检查 MyBatis 配置:确保 mybatis.config-locationmybatis.mapper-locations 配置正确,未覆盖拦截器。

Q2:如何拦截所有 SQL 执行(包括存储过程)?

  • 拦截 Executorqueryupdatecallable 方法(callable 对应存储过程),补充 @Signature

    @Signature(
            type = Executor.class,
            method = "callable",
            args = {MappedStatement.class, Object.class, ResultHandler.class}
    )
    

Q3:如何在拦截器中获取 HttpServletRequest(如用户信息)?

  • 通过 ThreadLocal 存储上下文信息(如在拦截器中设置):

    // 1. 定义 ThreadLocal 工具类
    public class RequestContextHolder {
        private static final ThreadLocal<HttpServletRequest> requestHolder = new ThreadLocal<>();
        public static void setRequest(HttpServletRequest request) { requestHolder.set(request); }
        public static HttpServletRequest getRequest() { return requestHolder.get(); }
        public static void clear() { requestHolder.remove(); }
    }
    
    // 2. 在 Spring MVC 拦截器中设置 request
    public class RequestInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            RequestContextHolder.setRequest(request);
            return true;
        }
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
            RequestContextHolder.clear(); // 防止内存泄漏
        }
    }
    
    // 3. 在 MyBatis 拦截器中获取
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        HttpServletRequest request = RequestContextHolder.getRequest();
        String username = request.getRemoteUser(); // 获取当前用户
        // ...
    }
    

人生不作安期生,醉入东海骑长鲸