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 {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) { return Plugin.wrap(target, this); }
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({}) public @interface Signature { Class<?> type();
String method();
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 { }
|
关键提醒: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 { private final Object target; private final Interceptor interceptor; 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; }
public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap) ); } return target; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
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]); } }
|
代理生成流程:
- 解析注解:
getSignatureMap() 解析拦截器的 @Intercepts 注解,生成 “目标对象类型→可拦截方法” 的映射;
- 匹配接口:
getAllInterfaces() 筛选目标对象实现的、且在拦截范围内的接口(JDK 动态代理需基于接口);
- 生成代理:若存在可拦截接口,通过
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<>();
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
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
| public StatementHandler newStatementHandler(Executor executor, MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { StatementHandler statementHandler = new RoutingStatementHandler( executor, ms, parameterObject, rowBounds, resultHandler, boundSql ); statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; }
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { Executor executor = ...; executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
|
(3)拦截链执行顺序
假设配置了两个拦截器:InterceptorA(拦截 StatementHandler.prepare ())和 InterceptorB(拦截 StatementHandler.prepare ()),配置顺序为 A→B,则代理链生成和执行顺序如下:
- 代理生成:原始对象 →
A 的代理 → B 的代理(最终返回 B 的代理);
- 方法调用:
B 的 invoke() → B 的 intercept() → A 的 invoke() → A 的 intercept() → 原始对象的 prepare();
- 结果返回:原始结果 →
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); }
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 { long start = System.currentTimeMillis(); try { return invocation.proceed(); } finally { 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;
@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 { @Override public void setProperties(Properties properties) { String logEnable = properties.getProperty("logEnable", "true"); System.out.println("SQL 耗时监控日志开关:" + logEnable); }
@Override public Object plugin(Object target) { return Plugin.wrap(target, this); }
@Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; String sqlId = ms.getId(); String sqlCommandType = ms.getSqlCommandType().name(); long startTime = System.currentTimeMillis();
try { return invocation.proceed(); } finally { 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"> <property name="logEnable" value="true"/> </plugin> </plugins>
</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
|
在拦截器中,经常需要读取或修改 MyBatis 核心对象的属性(如 StatementHandler 的 BoundSql、Executor 的 Transaction),但这些对象的属性多为 private,无法直接访问。MyBatis 提供 MetaObject 工具类,通过反射安全地操作对象属性,支持 OGNL 表达式。
1 2 3 4 5 6 7 8 9 10 11
| MetaObject metaObject = SystemMetaObject.forObject(target, configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
String sql = (String) metaObject.getValue("boundSql.sql"); Object parameter = metaObject.getValue("parameterHandler.parameterObject");
metaObject.setValue("boundSql.sql", modifiedSql);
|
在 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";
@Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject( statementHandler, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory() );
BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql"); String originalSql = boundSql.getSql();
if (originalSql.trim().toUpperCase().startsWith("SELECT")) { String modifiedSql = originalSql + " WHERE tenant_id = '" + tenantId + "'"; metaObject.setValue("boundSql.sql", modifiedSql); System.out.println("改写后 SQL:" + modifiedSql); }
return invocation.proceed(); }
}
|
拦截器开发注意事项
- 精确匹配方法签名:
@Signature 的 method 和 args 必须与目标方法完全一致(如 Executor.query() 有多个重载,需明确参数列表),否则拦截失效;
- 避免循环代理:不要拦截
Plugin 类或自身,否则会导致无限循环;
- 性能影响:拦截器会增加方法调用开销,避免在拦截逻辑中执行耗时操作(如 IO 操作);
- 线程安全:拦截器是单例对象,
setProperties 初始化的参数需保证线程安全(避免使用非线程安全的成员变量);
- 优先使用 MetaObject:操作 MyBatis 核心对象的属性时,必须通过
MetaObject,避免直接反射(MyBatis 版本升级可能导致属性名变化)。
总结
MyBatis 拦截器是 MyBatis 灵活性的核心体现,其底层基于 JDK 动态代理和责任链模式,允许开发者在 SQL 执行流程中插入自定义逻辑。关键要点:
- 拦截范围:仅支持拦截 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四大核心对象的特定方法;
- 开发规范:需实现
Interceptor 接口,并通过 @Intercepts/@Signature 声明拦截目标;
- 核心工具:
Plugin 生成代理,Invocation 封装拦截上下文,MetaObject 操作核心对象属性;
- 实战场景:日志记录、性能监控、SQL 改写、参数 / 结果加密等