0%

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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,声明该拦截器的拦截目标集合(支持同时拦截多个对象的多个方法):

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@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 执行前的改写):

1
2
3
4
5
6
7
8
9
10
11
// 声明该类是拦截器,指定拦截目标
@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 类核心源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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 核心源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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() 为对象生成代理链。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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() 可调用目标方法(或下一层代理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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() 触发后续逻辑,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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() 方法(覆盖查询和增删改操作),统计执行耗时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 全局配置文件中注册拦截器,并可设置自定义参数:

1
2
3
4
5
6
7
8
9
10
11
<configuration>
<!-- 配置拦截器链 -->
<plugins>
<plugin interceptor="com.example.interceptor.SqlExecuteTimeInterceptor">
<!-- 可选:为拦截器设置参数(通过 setProperties 方法注入) -->
<property name="logEnable" value="true"/>
</plugin>
</plugins>

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

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

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

1
2
3
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
2
3
4
5
6
7
8
9
10
11
// 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 条件(多租户场景):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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 改写、参数 / 结果加密等

欢迎关注我的其它发布渠道