mybatis自定义插件通过实现Interceptor接口,结合@Intercepts和@Signature注解拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler四大接口,在不修改源码的前提下,于sql执行关键节点插入逻辑,实现功能扩展、性能监控等;需注意调用invocation.proceed()、避免性能开销、处理多插件顺序及线程安全,并确保外部操作与事务一致性。
MyBatis自定义插件的核心,在于它提供了一种非侵入式的能力,让你能在MyBatis执行SQL的生命周期中,插入自己的逻辑。简单来说,它就像在MyBatis内部流程的几个关键节点上安插了“哨兵”,这些“哨兵”可以在数据准备、SQL执行、结果处理等环节,读取、修改甚至替换掉原本的行为,而这一切都不需要你动MyBatis源码一根指头。这对于功能扩展、性能监控、日志记录或者数据权限控制来说,简直是神来之笔。
解决方案
要编写一个MyBatis自定义插件,你主要需要实现MyBatis提供的
Interceptor
接口。这个接口有三个核心方法:
intercept
、
plugin
和
setProperties
。
- 实现
Interceptor
接口
:这是插件的骨架。 - 使用
@Intercepts
和
@Signature
注解
:这些注解是告诉MyBatis你的插件想要拦截哪个接口的哪个方法。一个插件可以拦截多个方法,甚至多个接口。-
@Intercepts
: 标记这是一个拦截器。
-
@Signature
: 定义要拦截的方法签名。
type
指定要拦截的接口(
Executor
,
ParameterHandler
,
ResultSetHandler
,
StatementHandler
之一),
method
指定该接口下的方法名,
args
指定该方法的参数类型列表。
-
- 实现
intercept(Invocation invocation)
方法
:这是插件的核心逻辑所在。invocation
对象包含了被拦截的目标对象(
getTarget()
)、被拦截的方法(
getMethod()
)以及方法参数(
getArgs()
)。你可以在这里执行你的业务逻辑,然后必须调用
invocation.proceed()
来放行,让MyBatis继续执行原来的流程,否则整个链条就断了。
- 实现
plugin(Object target)
方法
:这个方法的作用是,当MyBatis发现你的插件需要拦截某个目标对象时,会调用这个方法来生成一个代理对象。你通常会在这里返回Plugin.wrap(target, this)
,让MyBatis帮你生成一个代理,以便拦截器能够真正地拦截到目标对象的方法调用。
- 实现
setProperties(Properties properties)
方法
:这个方法用于在插件初始化时,接收你在MyBatis配置文件中为插件设置的属性。如果你需要一些配置参数,可以通过这里获取。 - 在
mybatis-config.xml
中注册插件
:最后一步,你需要告诉MyBatis你的插件在哪里。
这是一个简单的示例,展示如何拦截
Executor
的
update
方法,并打印SQL执行时间:
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({ @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 String authorName; // 演示如何获取配置属性 @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.currentTimeMillis(); try { // 这是最关键的一步,放行,让MyBatis继续执行原有的方法 Object result = invocation.proceed(); return result; } finally { long end = System.currentTimeMillis(); System.out.println("方法:" + invocation.getMethod().getName() + " 执行耗时:" + (end - start) + "ms. 作者: " + authorName); // 还可以获取更多信息,比如 invocation.getTarget() 得到被代理的Executor对象 // 或者 invocation.getArgs() 得到方法的参数 } } @Override public Object plugin(Object target) { // 返回一个代理对象,如果目标对象是Executor,就为其创建代理 // 这样MyBatis在调用Executor的方法时,就会先经过这个拦截器 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 在这里可以获取到MyBatis配置文件中为该插件设置的属性 this.authorName = properties.getProperty("author"); if (this.authorName == null) { this.authorName = "Unknown"; } System.out.println("PerformanceInterceptor 初始化,获取到属性 author=" + authorName); } }
然后在
mybatis-config.xml
中注册:
<configuration> <!-- ... 其他配置 ... --> <plugins> <plugin interceptor="your.package.PerformanceInterceptor"> <property name="author" value="YourNameHere"/> </plugin> </plugins> <!-- ... 其他配置 ... --> </configuration>
MyBatis插件能拦截哪些核心操作?
MyBatis的插件机制,本质上是基于JDK动态代理实现的。它允许你拦截MyBatis内部的四个核心接口。理解这些接口各自的职责,是写好插件的前提。
-
Executor
(执行器)
:这是MyBatis执行sql语句的核心。所有的SQL操作,无论是查询还是更新,最终都会通过Executor
来完成。拦截
Executor
,你可以在SQL执行前进行一些预处理,比如权限校验、数据源切换,或者在执行后进行性能统计。这是最常用的拦截点,因为它处于SQL执行的“大门口”。例如,你可以在这里获取到
MappedStatement
对象,从而知道当前执行的是哪个SQL。
-
ParameterHandler
(参数处理器)
:这个接口负责处理SQL语句的参数。它会把Java对象中的参数,正确地设置到JDBC的PreparedStatement
中。如果你需要对传入的参数进行加密、脱敏,或者进行一些复杂的参数转换,
ParameterHandler
-
ResultSetHandler
(结果集处理器)
:顾名思义,它负责处理JDBC返回的结果集(ResultSet
),并将其映射成Java对象。如果你需要对从数据库中读取出来的数据进行二次处理,比如解密、格式化,或者进行一些聚合操作,那么就应该拦截
ResultSetHandler
。这对于统一的数据清洗或视图层数据准备很有用。
-
StatementHandler
(语句处理器)
:这个接口负责构建JDBC的Statement
对象,包括SQL语句的准备、参数的设置以及结果集的处理。它更接近于JDBC层的操作。拦截
StatementHandler
,你可以在SQL语句真正发送到数据库之前,对其进行修改(比如动态添加
WHERE
条件)、分页SQL的重写,或者获取到最终要执行的SQL字符串。这对于实现复杂的分页逻辑或者多租户数据隔离尤其有用。
通常情况下,我们最常打交道的是
Executor
和
StatementHandler
,因为它们能让你在SQL执行的关键节点进行干预。而
ParameterHandler
和
ResultSetHandler
则更专注于数据的“进出”处理。
编写MyBatis插件时常见的“坑”有哪些?
写MyBatis插件,虽然功能强大,但确实有些地方稍不注意就会掉坑里。我个人就遇到过几次,挺让人抓狂的。
- 忘记调用
invocation.proceed()
intercept
方法里执行完自己的逻辑后,忘记调用
invocation.proceed()
,那么MyBatis的原始流程就会被中断,SQL根本不会被执行,或者结果集无法被正确处理。这就像你在一个流水线上,把东西拿过来检查了,但没放回传送带,后面的人就没法继续工作了。
-
ClassCastException
invocation.getTarget()
返回的是一个代理对象,它可能并不是你预期的原始对象类型。在某些情况下,你可能需要进行类型判断或者通过更通用的接口来操作。尤其是在处理
args
参数时,务必小心参数的类型和顺序,否则很容易在运行时抛出类型转换异常。
- 性能开销:插件会在每次被拦截的方法调用时执行。如果你在
intercept
方法里做了大量耗时操作,比如复杂的计算、网络请求或者文件I/O,那对整个应用的性能影响是巨大的。插件应该尽可能地轻量级,或者只在必要时才执行重型操作。我曾经见过有人在拦截器里做rpc调用,结果整个系统慢得像蜗牛。
- 多插件的执行顺序:当配置了多个插件时,它们的执行顺序是按照在
mybatis-config.xml
中配置的顺序来决定的。前一个插件的
plugin
方法会返回一个代理对象,这个代理对象又会作为下一个插件的
target
。如果插件之间有依赖关系,或者对同一个目标对象进行修改,顺序就变得非常重要。调试这种问题会比较头疼,因为你得理清代理链。
- 对
MappedStatement
的误解
:在拦截Executor
时,
MappedStatement
是一个非常重要的参数,它包含了SQL ID、SQL类型、参数映射等大量信息。但
MappedStatement
是MyBatis在启动时构建的,它是不可变的。如果你想修改SQL,不能直接修改
MappedStatement
,而是需要通过创建新的
MappedStatement
来替换,或者在
StatementHandler
中直接修改SQL字符串。直接修改
MappedStatement
的属性是无效的,因为你操作的是一个不可变对象的引用。
- 线程安全问题:如果你的插件内部持有状态(比如一个计数器、一个缓存),并且这个状态在多个线程之间共享,那么你必须确保它是线程安全的。否则,在高并发环境下,数据可能会出现混乱。大部分情况下,插件的
intercept
方法应该是无状态的,或者只处理当前请求的局部状态。
自定义插件如何与MyBatis的事务管理协同工作?
自定义MyBatis插件与事务管理协同工作,其实是一个比较自然的过程,因为插件本身就是MyBatis执行流程的一部分,它运行在MyBatis所管理的事务上下文之内。
-
事务边界内的操作:当你通过插件拦截
Executor
并执行SQL操作时,例如在
update
或
query
方法拦截器中进行数据修改,这些修改是默认包含在当前MyBatis事务中的。这意味着,如果外部调用者开启了一个事务,你的插件内部进行的任何数据库操作,都将受这个事务的控制。如果事务最终回滚,你的插件所做的修改也会随之回滚。这通常是我们期望的行为,保持了数据的一致性。
-
外部操作与事务:如果你的插件在
intercept
方法中,除了调用
invocation.proceed()
之外,还执行了额外的、与当前MyBatis事务无关的操作,比如:
- 向另一个数据库写入日志。
- 调用一个外部API。
- 写入文件系统。 这些操作不会被当前MyBatis事务管理。如果MyBatis事务回滚,这些外部操作不会自动回滚。在这种情况下,你需要自己考虑这些操作的原子性和一致性。你可能需要:
- 在插件内部开启一个独立的事务来管理这些外部操作。
- 使用消息队列等异步机制来解耦,或者通过补偿事务来处理回滚情况。
- 确保这些外部操作本身是幂等的,即使重复执行也不会导致问题。
-
Executor
拦截与事务控制:在
Executor
层面的拦截器,对于理解事务行为尤其重要。MyBatis的事务管理主要由
SqlSession
和其内部的
Executor
来协调。当你拦截
Executor
时,你实际上是在事务的“核心”部分进行操作。
- 你可以获取到当前
SqlSession
,甚至可以获取到其内部的
Transaction
对象(虽然直接操作
Transaction
不常见,但可以获取其状态)。
- 如果你在
intercept
方法中抛出异常,并且这个异常没有被捕获,MyBatis的事务管理器通常会捕获这个异常并触发事务回滚。这是MyBatis事务的默认行为,插件的行为会融入其中。
- 你可以获取到当前
总的来说,插件在MyBatis事务的“保护伞”下运行。大部分情况下,你不需要为插件的数据库操作单独考虑事务,它们会自然地融入当前会话的事务中。只有当你执行与MyBatis当前数据库连接无关的外部操作时,才需要特别注意事务边界和一致性问题。
评论(已关闭)
评论已关闭