MyBatis 拦截器(Interceptor)
概览
MyBatis 拦截器(又称插件)是其核心扩展机制,通过jdk动态代理(JDK Proxy)对 MyBatis 核心执行流程(SQL 执行、参数处理、结果集转换等)进行拦截,注入自定义逻辑。其核心是 org.apache.ibatis.plugin.Interceptor
接口,配合 @Intercepts
/@Signature
注解声明拦截规则,Invocation
封装调用上下文,Plugin
实现动态代理逻辑。
1. 使用场景
分页(可参考mybatis plus)
公共字段统一赋值
性能监控打印sql日志
权限等
2. mybatis 插件可拦截的方法
- 执行器( Executor(
org.apache.ibatis.executor.Executor
) ) - sql语法构建器
StatementHandler
- 参数处理器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:需求分析
- 拦截
Executor
的update
方法(覆盖所有 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 操作 |
---|---|---|---|
Executor | SQL 执行前/后(查询、增删改) | 统计执行时间、缓存控制、动态数据源路由 | getSqlCommandType() 、getTransaction() 、proceed() |
StatementHandler | Statement 创建/准备前/后 | 修改 SQL(多租户、分表)、记录原始 SQL | getBoundSql() 、setSql() 、prepare() |
ParameterHandler | 参数设置到 Statement 前 | 参数加密/解密、动态添加参数(如逻辑删除) | setParameters() 、getParameterObject() |
ResultSetHandler | 结果集转换为对象前/后 | 结果脱敏、数据格式转换(如 Date→String) | handleResultSets() 、getResultMaps() |
6. 进阶注意事项与踩坑指南
-
拦截器单例与线程安全:
拦截器是单例对象,实例变量必须是线程安全的(如final
修饰的配置参数、ThreadLocal),禁止定义可变状态(如private int count
,会导致多线程竞争)。 -
避免拦截代理对象:
拦截StatementHandler
或ParameterHandler
时,目标对象可能是代理(如分页插件生成的代理),直接操作会导致异常。需通过MetaObject
获取原始对象:// 正确获取原始 StatementHandler MetaObject metaObject = SystemMetaObject.forObject(statementHandler); StatementHandler originalHandler = (StatementHandler) metaObject.getValue("h.target");
-
修改 SQL 时注意参数对应:
若通过BoundSql.setSql()
修改 SQL(如添加条件),需确保参数个数与 SQL 占位符匹配,避免SQLSyntaxErrorException
或ParameterIndexOutOfBoundsException
。 -
Spring 依赖注入问题:
若拦截器需依赖 Spring Bean(如LogService
),需确保拦截器由 Spring 管理(如添加@Component
),避免手动new
导致依赖注入失败。 -
拦截顺序影响结果:
多个拦截器按配置顺序形成代理链,先配置的拦截器先执行前置逻辑,后执行后置逻辑。例如:
配置顺序:拦截器 A → 拦截器 B
执行顺序:A 前置 → B 前置 → 原始方法 → B 后置 → A 后置。
7. 调试技巧进阶
-
打印代理链结构:
在plugin
方法中打印目标对象类型,确认是否被代理:@Override public Object plugin(Object target) { logger.debug("目标对象类型:{},是否代理:{}", target.getClass().getName(), Proxy.isProxyClass(target.getClass())); return Plugin.wrap(target, this); }
-
查看最终执行的 SQL(含参数):
拦截StatementHandler.prepare
后,通过PreparedStatement
的toString()
查看参数填充后的 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; }
-
断点调试代理逻辑:
在intercept
方法中打断点,通过 IDE 的“查看变量”功能分析Invocation
的target
、method
、args
,定位问题。
8. 常见问题(FAQ)补充
Q1:拦截器在 Spring Boot 中不生效?
- 检查拦截器是否注册:通过
ConfigurationCustomizer
或@Component
注册; - 检查
@Signature
注解:args
类型需与目标方法完全匹配(如RowBounds
不能写成int
); - 检查 MyBatis 配置:确保
mybatis.config-location
或mybatis.mapper-locations
配置正确,未覆盖拦截器。
Q2:如何拦截所有 SQL 执行(包括存储过程)?
-
拦截
Executor
的query
、update
、callable
方法(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(); // 获取当前用户 // ... }