MyBatis Interceptor 深度解析与应用实践
一、MyBatis Interceptor概述
1.1 什么是MyBatis Interceptor
MyBatis Interceptor ,也称为MyBatis 插件 ,是 MyBatis 提供的一种扩展机制,用于在 MyBatis 执行 SQL 的过程中插入自定义逻辑。它类似于 AOP 中的切面或拦截器概念,可在不修改核心 MyBatis 源码的情况下,对核心对象(如执行器、语句处理器等)的方法调用进行拦截和增强。开发者通过实现 org.apache.ibatis.plugin.Interceptor
接口,并在类上使用 @Intercepts
注解指定需要拦截的方法签名,MyBatis 会在运行时为目标对象动态生成代理,在对应方法执行前后调用拦截器逻辑,实现诸如日志审计 、性能监控 、SQL 改写等功能。
简而言之,MyBatis Interceptor 是 MyBatis 提供的插件式拦截机制,允许开发者"插入"代码到 MyBatis 核心流程中,从而实现对 SQL 执行流程的监控、修改或增强。典型的拦截目标包括 SQL 执行前后的操作、参数绑定、结果处理等环节。与 Spring AOP 不同,MyBatis Interceptor 专注于 MyBatis 的内部执行过程,可看作 MyBatis 专属的"拦截器"。
1.2 MyBatis Interceptor的核心价值
MyBatis 拦截器的核心价值在于 灵活扩展和解耦关注点。通过拦截器,开发者可以将一些常见的横切关注点逻辑(如日志记录、性能统计、安全校验、动态 SQL 构建等)模块化,不侵入业务代码地整合到 MyBatis 层。这带来以下好处:
-
增强扩展能力:无需修改 MyBatis 核心源码,即可在查询、更新等关键时刻插入自定义逻辑。例如,可以记录每条 SQL 的执行时间、自动为分页添加限制、对敏感数据进行脱敏等。
-
关注点分离:将通用功能(如日志、监控、安全)从业务代码中剥离,使业务逻辑更清晰,同时方便统一维护和测试。
-
复用性:一个拦截器插件可以在不同项目中复用,只需根据需要注册即可,提升开发效率。
-
低侵入:与直接在业务层或 DAO 层写重复代码相比,拦截器只需配置一次即可全局生效,降低维护成本。
通过这些优势,MyBatis Interceptor 成为提升项目可维护性、可扩展性的重要工具,尤其适用于需要统一管控 SQL 行为的场景(如审计、安全、性能调优等)。
1.3 MyBatis Interceptor与其他框架的对比
MyBatis 拦截器与其他常见技术的比较主要体现在其拦截范围和方式上:
-
与 Spring AOP 对比 :Spring AOP 主要针对普通的 Java Bean 方法调用进行横切(如服务层或 DAO 层方法)。而 MyBatis 拦截器专注于MyBatis 的内部执行流程 ,拦截的是 MyBatis 核心接口的方法调用(如
Executor.update
、StatementHandler.prepare
等)。Spring AOP 通常需要在 Bean 方法上织入代理,而 MyBatis 插件直接对 MyBatis 框架级别的组件做代理,关注点更底层。二者可以结合使用,但职责不同。 -
与 Hibernate Interceptor 对比 :Hibernate 提供的
Interceptor
接口侧重于实体对象的生命周期(如保存、删除事件等),而 MyBatis 插件拦截的是 SQL 执行流程本身。Hibernate 拦截器更关注 ORM 映射层面,MyBatis 插件则对 SQL 和参数处理过程进行干预。 -
与 Servlet 过滤器对比:Servlet 过滤器作用于 Web 请求流程,拦截 HTTP 请求和响应;MyBatis 拦截器作用于数据库层,拦截 SQL 语句的准备和执行。尽管都是责任链模式的应用,但层级不同,一个是应用级别,一个是持久层级别。
综上,MyBatis Interceptor 属于 MyBatis 专用的插件化机制,定位于数据访问层的 SQL 执行流程管控,与其他通用 AOP 框架并不冲突,而是可补充和专注于 ORM 层面的需求。
二、MyBatis Interceptor原理详解
2.1 拦截器的工作机制
MyBatis 拦截器的工作机制基于 Java 动态代理 实现,主要涉及以下几个部分:
-
Interceptor 接口 :所有自定义拦截器需实现
org.apache.ibatis.plugin.Interceptor
接口,其中定义了intercept(Invocation)
、plugin(Object)
和setProperties(Properties)
三个方法。业务逻辑主要写在intercept
方法中,plugin
用于包装目标对象,setProperties
用于接收配置属性。 -
@Intercepts 注解 :自定义拦截器类上必须标注
@Intercepts({@Signature(...)})
注解,用以声明拦截哪些类型(Executor、StatementHandler 等)及其对应的方法签名。MyBatis 在加载拦截器时会读取注解信息,构建一个 signatureMap(映射要拦截的类到方法集合)。 -
InterceptorChain :配置解析完成后,MyBatis 将所有自定义的拦截器对象注册到
org.apache.ibatis.plugin.InterceptorChain
中,该链表按照配置顺序保存拦截器实例。 -
目标对象动态代理 :当 MyBatis 创建核心组件(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)时,会调用
InterceptorChain.pluginAll(target)
方法。该方法遍历拦截器链,对目标对象逐一执行interceptor.plugin(target)
,即对目标对象进行动态代理包装。实际上,plugin(target)
默认实现为Plugin.wrap(target, this)
,其中 Plugin 采用 JDK 代理创建一个代理对象,代理时会拦截实现了指定接口的方法调用。 -
Invocation 触发 :执行 SQL 操作时,实际调用会落到代理对象上。代理对象的
InvocationHandler
会判断当前方法是否在前面@Signature
指定的方法列表中,如果是则会构造一个Invocation
对象并调用interceptor.intercept(invocation)
;否则直接调用目标对象的原方法。Invocation
封装了目标对象(target)、方法(method)和参数(args),并提供proceed()
方法用于继续调用下一个拦截器或最终的目标方法。
流程示例:在执行 Executor.update()
时,实际会调用动态代理对象的 invoke
方法,该方法检测到 update
是拦截目标,就执行自定义拦截器的 intercept
逻辑。在自定义拦截器中可以在 Invocation.proceed()
前后添加自己的处理。最终,当所有拦截器链条执行完毕后,才真正调用底层的 Executor 完成数据库操作。
2.2 拦截器的执行流程(结合源码分析)
结合源码,可总结 MyBatis 拦截器的执行流程如下:
-
加载注册拦截器 :在解析 MyBatis 配置文件时,识别
<plugins>
节点并实例化配置的拦截器类(自动调用无参构造),然后调用Configuration.addInterceptor(interceptor)
将其加入InterceptorChain
(此时addInterceptor
内部其实是调用InterceptorChain.addInterceptor
,见源码[20])。 -
创建核心对象并包装 :当 MyBatis 初始化时创建
Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
等对象时,会调用configuration.getInterceptorChain().pluginAll(target)
。例如在DefaultSqlSession
中创建Executor
后即包装之(伪代码示例):Executor executor = new SimpleExecutor(...); executor = (Executor) configuration.getInterceptorChain().pluginAll(executor);
此时,InterceptorChain 会遍历所有注册的拦截器,对 executor 对象进行包装,形成多层动态代理:
target = interceptor1.plugin(target); target = interceptor2.plugin(target); // ...
具体源码中
pluginAll
方法显示:public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
-
动态代理拦截 :
Interceptor.plugin(Object target)
的默认实现是Plugin.wrap(target, this)
(见)。Plugin.wrap
方法首先通过getSignatureMap(interceptor)
读取拦截器类上的@Intercepts
注解信息,得到一个映射关系,指明要拦截的类型及方法集合。然后,判断目标对象是否实现了这些接口中的任意一个:如果有匹配,则通过Proxy.newProxyInstance
创建代理对象;否则直接返回原对象。 -
代理对象的
invoke
逻辑 (见源码):当代理对象的方法被调用时,会进入Plugin.invoke
方法。该方法从signatureMap
中取出本次调用的目标方法所在接口的Method
集合,如果调用的方法在其中,则执行interceptor.intercept(new Invocation(target, method, args))
;否则调用method.invoke(target, args)
直接执行原方法。如下关键片段:@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 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); }
-
执行自定义逻辑并继续调用 :进入
interceptor.intercept(Invocation)
后,即执行用户在拦截器中实现的逻辑。Invocation
对象封装了target
、method
、args
,可通过调用invocation.proceed()
来继续调用下一个拦截器链条或目标方法。在intercept
方法内部,开发者可以在invocation.proceed()
前后添加自己的业务逻辑,例如记录日志、修改参数、性能计时等。Invocation.proceed()
最终会调用原始对象的方法(或下一个拦截器),完成真实的数据库操作。
以上流程说明了 MyBatis 插件机制的核心原理:通过动态代理将多个拦截器按配置顺序"串联"在目标对象上,调用方法时逐个进入拦截器的 intercept
方法,最后执行目标方法。这样就形成了一条拦截器链(责任链模式)的执行路径,实现了对 MyBatis SQL 执行过程的灵活增强。
2.3 拦截器的生命周期管理
MyBatis 拦截器的生命周期主要由 MyBatis 框架自行管理,特征如下:
-
单例模式 :在配置加载阶段,MyBatis 通过反射创建拦截器对象(通常是单例的,可以在 MyBatis 配置或 Spring 容器中配置)。一个
SqlSessionFactory
生成期间,配置的每个拦截器类只会被实例化一次,保存到InterceptorChain
中。之后执行的所有 SQL 操作都共享这些拦截器实例,因此要求拦截器实现是线程安全的(参见 5.2 小节)。 -
配置属性注入 :对于
<plugin>
配置中的<property>
属性,MyBatis 在创建拦截器实例后会调用setProperties(Properties)
方法,将配置的属性传入拦截器,以便初始化参数或外部配置。可以利用这一机制动态改变拦截器行为。 -
与 SqlSessionFactory 关联 :拦截器实例存储在
Configuration
对象中,Configuration
决定了SqlSessionFactory
的行为。只有在配置了特定拦截器时,才会加入拦截链。通常情况下,拦截器生命周期等同于整个SqlSessionFactory
生命周期。 -
支持热重载(可选):在开发环境或动态运行时,部分项目可能通过自定义手段实现拦截器的热插拔,例如重新加载配置或用 Spring 上下文刷新插件。MyBatis 本身并不内置拦截器的热重载机制,但由于可编程性强,可以结合 Spring 等框架提供类似功能。
总体上,MyBatis 拦截器由框架自动创建、配置与调用,开发者无需手动管理其创建和销毁。需要注意的是,由于其单例且跨线程调用的特性,在设计拦截器时应避免使用非线程安全的可变共享状态。后续5.2节将详细讨论拦截器的线程安全及状态管理策略。
三、自定义拦截器开发指南
开发自定义 MyBatis 拦截器需要关注注解配置、核心方法实现、注册方式和测试等方面。以下将逐步介绍具体步骤和注意事项。
3.1 拦截器注解的使用规范
自定义拦截器类必须使用 @Intercepts
注解来声明要拦截的目标接口和方法。常见使用示例如下:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExampleInterceptor implements Interceptor {
// ...
}
注解说明:
-
@Intercepts :用于标记该类是 MyBatis 拦截器,包含多个
@Signature
条目。 -
@Signature 属性:
-
type
:要拦截的接口类型(常见有Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
等)。 -
method
:接口中的方法名(字符串)。 -
args
:方法参数类型数组,对应该方法的参数列表。类型必须准确,否则会找不到对应方法。
-
多组 @Signature
表明一个拦截器可以拦截多个目标方法。使用时需确保签名对应的方法在指定接口中存在。例如 Executor.update(MappedStatement, Object)
表示拦截 Executor 的 update 方法。MyBatis 在构建 signatureMap
时会通过反射获取指定类型的 Method
对象(见源码mybatis.org),找不到会报错。
常用拦截类型与方法:
-
Executor
(执行器):update
,query
,flushStatements
,commit
,rollback
,getTransaction
,close
,isClosed
等。 -
StatementHandler
(语句处理器):prepare
,parameterize
,batch
,update
,query
等。 -
ParameterHandler
(参数处理器):getParameterObject
,setParameters
。 -
ResultSetHandler
(结果集处理器):handleResultSets
,handleOutputParameters
。
可根据需要选择合适的类型和方法。以下示例展示了注解声明及拦截器类定义的典型结构:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {
// 实现拦截逻辑
}
3.2 拦截器核心方法的实现要点
自定义拦截器需要实现 Interceptor
接口,其核心方法包括:
-
intercept(Invocation invocation)
:拦截逻辑的入口。Invocation
对象封装了当前拦截的目标对象(target
)、方法(method
)和参数(args
)。开发者应在此方法中编写自定义处理逻辑,然后通过invocation.proceed()
继续调用下一个拦截器或目标方法。示例实现:@Override public Object intercept(Invocation invocation) throws Throwable { // 前置处理逻辑(例如日志、参数检查等) System.out.println("==> 执行 SQL 之前"); // 执行下一个拦截器或实际方法 Object result = invocation.proceed(); // 后置处理逻辑(例如结果处理、性能统计等) System.out.println("==> 执行 SQL 之后"); return result; }
关键点:
-
调用
proceed()
:必须调用invocation.proceed()
以继续执行链条,否则会阻断 SQL 的执行。可以在proceed()
前后分别实现前置和后置逻辑。 -
返回值 :通常将
invocation.proceed()
的返回值原样返回,或在其基础上进行修改(例如对查询结果进行包装、过滤等)。 -
异常处理 :
intercept
方法签名允许抛出Throwable
,出现异常时可捕获并根据需要处理或包装后抛出。异常会传递给调用者,并可在 MyBatis 层进行统一处理。
-
-
plugin(Object target)
:用于将拦截器应用于目标对象。通常实现只需调用Plugin.wrap(target, this)
。该方法会根据前述签名决定是否生成代理对象。示例:@Override public Object plugin(Object target) { // 将拦截器与目标对象通过动态代理绑定 return Plugin.wrap(target, this); }
一般无需修改此逻辑,但在特殊场景下可以增加对代理创建过程的自定义控制。
Plugin.wrap
方法内部会检查目标对象是否实现了签名中指定的接口,若符合条件则生成代理,否则返回原对象。 -
setProperties(Properties properties)
:接收<plugin>
配置中的属性,用于配置拦截器。例如可以通过properties.getProperty("key")
获取指定属性值并初始化拦截器。示例:private Properties properties; @Override public void setProperties(Properties properties) { this.properties = properties; // 可读取属性进行初始化 String logLevel = properties.getProperty("logLevel", "INFO"); System.out.println("设置拦截器属性:logLevel=" + logLevel); }
在 MyBatis 配置文件中使用时,可为每个
<plugin>
标签添加<property name="..." value="..."/>
来传递参数。
3.3 拦截器配置与注册方法
自定义拦截器需在 MyBatis 配置中注册,常见方式有两种:
-
MyBatis XML 配置 (适用于无 Spring 场景):在
mybatis-config.xml
的<plugins>
节点中添加<plugin>
配置。例如:<plugins> <plugin interceptor="com.example.MyInterceptor"> <property name="threshold" value="1000"/> <property name="logSql" value="true"/> </plugin> </plugins>
上例将
com.example.MyInterceptor
添加到拦截链,同时传入两个配置属性(可以在setProperties
方法中读取)。注意interceptor
属性填写拦截器类的完整类名。 -
Spring Boot 或 Spring 集成:在 Spring 环境中可以通过 Java 配置或 Bean 注册拦截器。常用方式包括:
-
SqlSessionFactoryBean/SqlSessionTemplate 设置 :在 Spring 配置类中,获取
SqlSessionFactoryBean
或SqlSessionFactory
,然后调用setPlugins
方法注入拦截器数组。例如:@Configuration public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setPlugins(new Interceptor[]{ new MyInterceptor() }); return factoryBean.getObject(); } }
这样
MyInterceptor
将被注册为 MyBatis 的插件。 -
MyBatis-Spring-Boot-Starter 方式(自动扫描 Bean):若使用 MyBatis Spring Boot Starter,可简单地将拦截器声明为 Spring Bean,例如在配置类中:
@Bean public MyInterceptor myInterceptor() { MyInterceptor interceptor = new MyInterceptor(); Properties props = new Properties(); props.setProperty("threshold", "1000"); interceptor.setProperties(props); return interceptor; }
MyBatis-Spring 将自动将所有实现了
Interceptor
接口的 Bean 加入拦截器链。部分新版 Starter 也支持通过application.yml
指定插件,但更常见的还是通过 Bean 注入完成。
-
拦截器调用链测试用例:为了验证拦截器的功能,通常会编写单元测试。例如,以下示例测试创建一个简单目标接口,并在调用时确保拦截器被触发:
public interface Foo {
void bar();
}
public class FooImpl implements Foo {
@Override
public void bar() {
System.out.println("原始业务方法 bar() 执行");
}
}
@Intercepts({
@Signature(type = Foo.class, method = "bar", args = {})
})
public class FooInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("==> FooInterceptor:方法调用之前");
Object result = invocation.proceed();
System.out.println("==> FooInterceptor:方法调用之后");
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
// JUnit 测试代码
public class InterceptorTest {
@Test
public void testFooInterceptor() {
Foo target = new FooImpl();
Foo proxy = (Foo) Plugin.wrap(target, new FooInterceptor());
proxy.bar();
// 预期输出:
// ==> FooInterceptor:方法调用之前
// 原始业务方法 bar() 执行
// ==> FooInterceptor:方法调用之后
}
}
上述测试中,FooImpl.bar()
的调用被 FooInterceptor
成功拦截,从而在方法前后输出了额外日志,验证了拦截器调用链的正确性。
四、MyBatis Interceptor应用场景
MyBatis 拦截器在实际项目中有多种应用场景,以下介绍常见几类,并提供示例代码。
4.1 SQL分页插件开发
场景需求 :在查询时自动为 SQL 添加分页参数,无需在 XML 或注解中手动拼接分页条件。常见做法是拦截语句处理器,在 StatementHandler.prepare()
阶段修改即将执行的 SQL。
实现思路 :拦截 StatementHandler.prepare(Connection, Integer)
方法,获取原始 SQL 并在其后添加分页语句(以 MySQL 为例为 LIMIT offset, size
)。可以通过反射修改 BoundSql
中的 SQL 字符串。示例:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {
private static final String MYSQL = "mysql";
// 从配置或上下文中获取当前分页参数
private int offset;
private int limit;
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 使用反射获取实际的RoutingStatementHandler和BoundSql
BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql().trim();
// 根据数据库方言构建分页 SQL
String pageSql = originalSql + " LIMIT " + offset + ", " + limit;
// 通过 MetaObject 修改原 BoundSql 的 SQL
MetaObject metaStatementHandler = SystemMetaObject.forObject(handler);
metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);
System.out.println("分页拦截器修改SQL: " + pageSql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.offset = Integer.parseInt(properties.getProperty("offset", "0"));
this.limit = Integer.parseInt(properties.getProperty("limit", "10"));
}
}
注册配置示例(mybatis-config.xml):
<plugins>
<plugin interceptor="com.example.PaginationInterceptor">
<property name="offset" value="0"/>
<property name="limit" value="20"/>
</plugin>
</plugins>
上述拦截器在每次 prepare
时自动为 SQL 追加 LIMIT
分页语句,实现了简易的服务器端分页功能。实际使用中还可结合 RowBounds
或其它分页参数动态设置 offset
、limit
。
4.2 SQL日志记录与审计
场景需求:记录所有执行的 SQL 语句及其参数,方便调试和审计。可以在每次查询或更新前后打印 SQL。
实现思路 :拦截 StatementHandler
的 prepare
方法获取 SQL,也可以拦截 Executor.query/update
方法获取参数或计时。示例如下,在 prepare
阶段日志输出 SQL:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare",
args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql().replaceAll("\\s+", " ");
System.out.println("[SQL日志] 执行 SQL: " + sql);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
使用示例:
<plugins>
<plugin interceptor="com.example.SqlLogInterceptor"/>
</plugins>
每次执行 SQL 时,上述拦截器都会打印标准化后的 SQL 语句到控制台或日志文件。结合日志框架(如 Log4j、SLF4J)可将输出记录到文件或监控系统中。
4.3 性能监控与调优
场景需求:监控 SQL 执行时间、慢查询告警或统计接口响应时间,以便性能优化。
实现思路 :拦截执行语句的环节(如 StatementHandler.query/update
或 Executor.query/update
),在 intercept
方法中记录开始时间和结束时间,计算耗时,超过阈值时打印警告。示例代码:
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {
private long threshold = 500; // 毫秒
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long end = System.currentTimeMillis();
long time = end - start;
if (time > threshold) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
System.err.println("[性能警告] SQL 执行超时 " + time + " ms - "
+ ms.getId());
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.threshold = Long.parseLong(properties.getProperty("threshold", "500"));
}
}
注册示例:
<plugins>
<plugin interceptor="com.example.PerformanceInterceptor">
<property name="threshold" value="300"/>
</plugin>
</plugins>
该拦截器对所有 Executor.update
和 Executor.query
进行监控,记录每次调用耗时。当耗时超过配置的阈值(如300毫秒)时,输出警告信息。这样可用于统计慢查询,并在开发或生产环境中给予提示,帮助定位性能瓶颈。
4.4 数据脱敏与安全校验
场景需求:在从数据库查询并返回结果前,对敏感数据进行脱敏处理(如隐去身份证号中间位、手机号后几位),或对输入参数做安全校验。
实现思路 :可以拦截 ResultSetHandler.handleResultSets(ResultSet)
方法,对返回的结果集进行遍历和处理;也可以拦截 ParameterHandler.setParameters
方法,对 SQL 参数进行校验或修改。下面演示对返回结果进行脱敏的拦截器示例:
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets",
args = {Statement.class})
})
public class DataMaskInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<Object> results = (List<Object>) invocation.proceed();
if (results != null) {
for (Object obj : results) {
if (obj instanceof User) { // 假设返回的是 User 对象列表
User user = (User) obj;
String email = user.getEmail();
// 简单示例:只显示邮箱前3位,其余替换为星号
if (email != null && email.length() > 3) {
user.setEmail(email.substring(0, 3) + "****");
}
}
}
}
return results;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
在这个示例中,对返回的 List<Object>
进行遍历,如果元素是 User
对象,就对其 email
字段进行脱敏处理。注册该拦截器后,所有查询结果中的 email
字段都会在返回给调用方之前经过该逻辑脱敏。类似地,可以在插入或更新前校验参数或在查询前校验用户权限等。
4.5 动态SQL构建与参数处理
场景需求:在不修改原 SQL 的情况下,根据某些条件动态更改 SQL 或参数。例如在执行前自动为 SQL 追加查询条件,或修改输入参数。
实现思路 :可拦截 Executor.update
或 Executor.query
,分析 MappedStatement
和参数对象,构造新的 SQL 或参数,然后通过 MappedStatement
的 SqlSource
和 BoundSql
生成新的 MappedStatement
。示例思路(简化版):
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DynamicSqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 根据业务需求动态生成或修改 SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 例如如果参数中包含某个标志,则追加条件
if (parameter instanceof Map && ((Map) parameter).containsKey("activeOnly")) {
String newSql = originalSql + " AND active = 1";
// 这里为了简单示例,不做 MappedStatement 完全复制
// 在实际中可使用 MetaObject 修改boundSql.sql字段
System.out.println("[动态SQL拦截] 新SQL: " + newSql);
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {}
}
在实际生产场景,完全复制并替换 MappedStatement
及其 SqlSource
需要较多代码(可参考 MyBatis 源码中的插件实现逻辑),上述示例仅演示概念。在需要动态 SQL 时,常见做法是结合 MyBatis 提供的 <if>
标签或 SqlSessionFactory
监听等,但拦截器也可实现更灵活的逻辑。
五、MyBatis Interceptor性能优化
虽然拦截器功能强大,但错误或过多使用会带来性能负担。以下优化技巧可供参考。
5.1 拦截器链的性能瓶颈分析
-
动态代理开销 :每个拦截目标都会被多重代理包装,方法调用需经过多次
InvocationHandler.invoke
调用,增加了额外的反射开销。随着拦截器数量增多,这种链式包装带来的开销成倍增长。 -
频繁构造对象:如果在拦截器中频繁创建对象(例如每次调用都 new 复杂对象),会增加垃圾回收压力。特别是处理大量 SQL 时,应尽量重用可复用对象或使用局部缓冲区。
-
数据结构复杂性:如果拦截器内部逻辑复杂,如使用了深度反射或大规模集合遍历,会加长单次调用时间。
优化建议:
-
仅拦截必要方法:注解中尽量精确指定需要拦截的方法,避免对不需要的接口进行拦截。
-
合并相似拦截器 :如果多个拦截器作用相似,可合并为一个拦截器,在
intercept
内部分支处理,减少代理层数。 -
提前过滤 :在
intercept
中尽早检查条件,不满足时尽快返回invocation.proceed()
,减少不必要的处理。 -
懒加载:对于某些复杂计算,可考虑仅在满足特定条件时才执行,避免每次都运行所有代码。
5.2 拦截器的线程安全设计
拦截器实例通常是单例并在多线程环境中被复用,因此需保证线程安全:
-
无状态设计 :尽量不在拦截器中定义可变成员变量,或将其限定为
final
常量(例如数据库方言常量)。避免使用非线程安全的数据结构(如非同步的ArrayList
等)作为字段。 -
局部变量 :在
intercept
方法中使用局部变量存储临时数据,局部变量天然线程隔离。 -
线程局部变量 :如果需要在线程间隔离数据,可使用
ThreadLocal
,但需注意可能导致内存泄漏(一定要在请求结束时清理)。 -
同步锁避免 :尽量避免在拦截器中引入锁(如
synchronized
)会严重影响并发性能。如需共享资源,可考虑使用无锁并发容器或外部缓存系统。
5.3 拦截器的缓存策略
拦截器自身可以引入合理的缓存策略来提升性能:
-
方法/元数据缓存 :对于反射获取的方法或配置,可以在拦截器加载时缓存
Method
对象。MyBatisPlugin
已经将匹配的方法缓存到signatureMap
中,无需重复查找。 -
SQL 模板缓存:若动态修改 SQL 的逻辑复杂,可以将常用的 SQL 模板或修改后 SQL 保存到缓存,减少每次重组 SQL 的开销。
-
结果/参数缓存:如果某些拦截逻辑依赖于计算结果(如脱敏规则表、加密密钥等),可将此类静态信息缓存到拦截器实例中,避免频繁读取外部资源。
需要注意避免缓存失效带来的一致性问题。任何缓存必须与底层数据更新机制兼容,例如在数据表变化后清空相关缓存或使用缓存穿透策略。
5.4 拦截器的异步执行方案
对于一些非关键路径的耗时操作,如日志记录、审计信息存储等,可考虑异步执行以减少对主线程的阻塞:
-
日志异步化 :在拦截器
intercept
中生成要记录的日志内容后,使用异步框架(如异步队列、独立线程、消息系统)将日志写入任务推送出去,不在当前线程写文件或数据库。 -
审计信息异步化:类似日志,将审计数据封装后放入线程池或消息中间件,让后台进程处理。
-
定时任务:对于周期性或批量统计,也可在拦截器中简单收集数据(如计数、时间等),而真正的汇总和告警由定时任务完成。
异步策略需注意线程安全 和异常处理,确保异步任务失败不会影响主流程,以及及时处理失败的任务。
六、MyBatis Interceptor的局限性与替代方案
6.1 拦截器的局限性分析
虽然 MyBatis 插件非常灵活,但也存在一些局限,需要合理评估:
-
作用范围有限 :MyBatis 拦截器只能拦截
Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
这四类接口的方法。对于其他自定义的业务方法、非 MyBatis 的流程,拦截器无能为力。 -
签名硬编码:拦截方法需要在注解中硬编码指定方法名和参数类型,不能模糊匹配或使用表达式。一旦对应方法重命名或参数改变,拦截配置就会失效或抛出异常。
-
与插件冲突:多个插件如果拦截了同一目标,执行顺序取决于配置顺序(默认后配置的先执行,见第8.1节),容易造成难以预测的交互问题。不同插件间的相互影响需人工管理。
-
调试困难:由于拦截器通过代理隐式工作,对链条调试不直观。排查问题时需依赖日志或断点,复杂场景下分析链路较为繁琐。
-
性能开销:如前所述,每个拦截器会带来额外方法调用开销,过多插件会明显拖慢执行速度,需要谨慎使用。
6.2 MyBatis Interceptor与Spring AOP的对比
对比 MyBatis 插件与 Spring AOP,可从下面几个方面进行分析:
特性 | MyBatis Interceptor | Spring AOP |
---|---|---|
织入对象 | MyBatis 核心接口(Executor、StatementHandler 等) | Spring 管理的 Bean 方法 |
拦截点粒度 | SQL 执行前后、参数绑定、结果处理等数据库层面 | 方法调用层面,可拦截任何 public 方法(默认) |
配置方式 | MyBatis 配置文件或 MyBatis-Spring 插件方式 | 注解或 XML 配置切面(@Aspect、XML) |
实现方式 | 动态代理/反射(不依赖 AOP 框架) | 代理(JDK/CGLIB)或字节码增强 |
依赖环境 | 必须在 MyBatis 环境中使用 | 任意 Spring 应用,不依赖数据库 |
执行时机 | 数据库操作时(SQL 执行链) | 方法执行时 |
使用场景 | 日志、SQL 重写、性能监控、数据过滤等 | 事务、权限、安全、通用日志等 |
两者并不冲突,在一个项目中可以同时使用:Spring AOP 用于业务层、服务层的切面逻辑,而 MyBatis 插件专注于数据访问层面。例如,在业务方法前可以用 AOP 进行权限检查,用 MyBatis 插件记录 SQL 日志。
6.3 替代方案:基于责任链模式的自定义实现
当 MyBatis 插件的局限无法满足需求时,可以考虑自定义实现责任链模式来达到类似拦截的效果。例如:
-
手动调用链:在 DAO 层或服务层封装一个调用流程,将多个"处理器"串联,每个处理器实现某个前置或后置逻辑。通过在执行数据库操作前后手动调用这些处理器,实现与拦截器类似的效果。
-
使用 Spring AOP :如果业务方法正好位于 Spring 管理的 Bean 中,也可以使用 Spring AOP 切面拦截 DAO 层的方法,并在切面中操作
SqlSession
或参数。Spring AOP 的切面灵活度高,但无法像 MyBatis 插件那样直接操作 SQL。 -
代理包装 SqlSession :自行对
SqlSession
或 Mapper 接口进行代理,插入拦截逻辑。例如使用 JDK 动态代理或 CGLIB 对 Mapper 接口生成代理类,在调用select
/update
方法时执行预处理或后处理。
这些方案相比 MyBatis 插件更为应用层,需要自行维护链条调用逻辑和顺序。虽然开发成本可能更高,但在某些特殊场合(如需要插入无法通过 MyBatis API 访问的步骤)提供了替代可能。
七、MyBatis Interceptor源码深度解析
进一步深入 MyBatis 源码,可了解拦截器机制的实现细节。以下重点解析关键类:InterceptorChain
、Plugin
和 Invocation
。
7.1 InterceptorChain源码解析
org.apache.ibatis.plugin.InterceptorChain
维护了所有拦截器实例列表,并负责对目标对象进行包装。其源码核心如下:
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);
}
}
-
interceptors 列表:存储所有已注册的拦截器,添加顺序即配置顺序。
-
addInterceptor() :在
Configuration
解析<plugin>
时调用,将新拦截器加入列表。 -
pluginAll(Object target) :将给定对象(如 Executor)依次传递给每个拦截器的
plugin
方法。plugin
返回一个可能的代理对象,因此最终得到的是多层嵌套的代理。
该设计确保了多个拦截器可以组合使用。顺序上,第一个拦截器的 plugin
先被应用(最里层代理),最后一个拦截器包裹在最外层,导致配置文件中后注册的拦截器先执行的效果(详见第8.1节)。
7.2 Plugin类的动态代理实现
Plugin
类实现了 InvocationHandler
,负责创建并处理代理对象的调用。源码关键部分:
public class Plugin implements InvocationHandler {
private final Object target;
private final Interceptor interceptor;
private final Map<Class<?>, Set<Method>> 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 {
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);
}
// ... 获取signatureMap和getAllInterfaces方法 ...
}
-
wrap() 方法 :获取拦截器的
@Signature
信息构造signatureMap
,再查找目标类实现的接口,将匹配接口传入Proxy.newProxyInstance
构造代理。只有当目标类实现了注解中指定的接口时,才会生成代理;否则返回原始对象。 -
invoke() 方法 :每次代理对象调用方法时都会进入此处。根据
method.getDeclaringClass()
在signatureMap
查找对应的方法集合,如果当前方法匹配,则构造Invocation
调用拦截器的intercept
;否则直接调用目标对象的方法。 -
getSignatureMap() :解析拦截器类上的
@Intercepts
注解,将每个@Signature
的type
、method
和args
转为java.lang.reflect.Method
并存入Map<Class<?>, Set<Method>>
中。这样每个目标接口对应一个需要拦截的方法列表。 -
getAllInterfaces() :收集目标类及其父类实现的所有接口,筛选出包含在
signatureMap
中的接口,用于代理实现。
通过 Plugin.wrap
,MyBatis 能够在运行时动态为目标对象生成代理对象,实现对指定方法的拦截。代理对象与目标对象实现了相同接口,调用时透明地进入 Plugin.invoke
,从而触发开发者编写的 intercept
逻辑。
7.3 Invocation调用链的执行机制
Invocation
是拦截器执行过程中的上下文对象,源码(见 MyBatis Javadoc)简单描述如下:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) { ... }
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args);
}
// 还有 getTarget(), getMethod(), getArgs() 等辅助方法
}
-
作用:封装了拦截点的信息,包含目标对象、目标方法和调用参数。
-
proceed() 方法 :当开发者在
intercept
方法中调用invocation.proceed()
时,会继续调用目标方法或下一个拦截器。实际上,Invocation
持有的target
是原始对象或下一个代理实例,因此调用method.invoke(target, args)
会进入拦截链的下一层,最终到达最里层目标实现。 -
调用链 :如果有多个拦截器链在一个方法上,则每次
proceed()
都会触发链中下一个拦截器的intercept
;最后一个拦截器的proceed()
会调用原始目标的方法并返回结果。
在实际代码中,我们通常不需要直接操作 Invocation
以外的部分,只要遵循**在拦截方法内适时调用 invocation.proceed()
**即可实现链式调用。例如,一个简单的 intercept
中可能是:
public Object intercept(Invocation invocation) throws Throwable {
// 拦截前逻辑
Object result = invocation.proceed(); // 执行下一个拦截器或原方法
// 拦截后逻辑
return result;
}
Invocation
的作用机制确保了 责任链模式 的串联执行特性:多个拦截器按顺序包裹目标,并逐个执行前置和后置逻辑。
八、进阶:拦截器链的执行顺序控制
8.1 拦截器优先级的配置方法
MyBatis 不提供显式的优先级配置注解。拦截器的执行顺序仅通过配置顺序控制:在 mybatis-config.xml
中 <plugins>
节点配置多个 <plugin>
时,后声明的拦截器会先执行 ,这与 InterceptorChain.pluginAll()
的实现顺序相符(后加入的拦截器处于列表后端,调用时代理层数最外层,先被触发)。例如:
<plugins>
<plugin interceptor="com.example.InterceptorA"/>
<plugin interceptor="com.example.InterceptorB"/>
</plugins>
在这个配置中,InterceptorB
会在 InterceptorA
之前执行(因为 B 被包装在 A 的外层)。如果需要调整顺序,只需调整 <plugin>
标签的顺序即可。MyBatis 官方论坛和文档也说明了这一点。
8.2 拦截器链的调试技巧
调试拦截器链可以采取以下方法:
-
日志输出 :在每个拦截器的
intercept
方法中添加日志打印,标识进入和退出方法的位置,或者打印拦截器名称和方法名。通过日志可了解链条中各拦截器执行的先后顺序以及方法调用情况。 -
使用断点 :在 IDE 中为每个拦截器类设置断点,调试时观察调用栈和拦截链情况。由于链式调用较复杂,可在
Plugin.invoke
中断点查看Invocation
对象内容,确认目标方法的真正执行过程。 -
MyBatis 调试日志 :启用 MyBatis SQL 日志(在
log4j.properties
或application.yml
中设置 log level 为 DEBUG),可以看到代理生成的 SQL 语句执行日志,间接反映拦截器是否修改了 SQL。 -
测试覆盖 :编写单元测试对每个拦截器方法进行覆盖测试,确保链路中各拦截器都按预期运行。可以使用 Mockito 等框架模拟
Invocation
对象来测试intercept
方法的逻辑。
8.3 拦截器链的异常处理机制
-
异常传播 :如果拦截器的
intercept
方法中抛出异常,MyBatis 会将该异常向上抛出,最终由调用方捕获或传播。开发者应根据需要捕获并处理拦截器中的异常,或者将其封装为自定义异常抛出。 -
拦截器内部处理 :可以在
intercept
方法内部使用try-catch
捕获异常,对可预见的错误进行处理,并决定是否重新抛出。例如,日志拦截器遇到打印错误时一般应捕获异常避免影响业务流程。 -
链中断:一旦链中某个拦截器出现未捕获异常,后续拦截器将不会执行,直接中断整个 SQL 调用。需要注意配置顺序和异常可能造成的影响范围。
-
MyBatis 异常转化 :在
Plugin.invoke
中(源码)会捕获底层方法抛出的异常并使用ExceptionUtil.unwrapThrowable
进行解包抛出,以保证抛出的是可理解的异常类型。因此,在拦截器中如果使用反射或代理调用目标方法而产生InvocationTargetException
,最终调用方看到的将是其根本原因。
合理的异常处理策略可以保证拦截器出现问题时,系统能快速定位并采取措施,而不会因为拦截器问题导致更严重的后果。