MyBatis 插件为什么这么强大?一次看懂拦截器机制
大家都知道 MyBatis 有一个非常强大的功能:
插件(Plugin)。
很多框架都在用它:
- MyBatis-Plus 分页插件
- 数据权限插件
- SQL 审计插件
- 多租户插件
- SQL 性能分析插件
甚至很多公司都会自己开发 MyBatis 插件。
但是问题来了:
一个普通的插件,为什么能修改 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 {
System.out.println("SQL执行前");
Object result = invocation.proceed();
System.out.println("SQL执行后");
return result;
}
}
配置后:
<plugins>
<plugin interceptor="com.demo.SqlLogInterceptor"/>
</plugins>
执行查询:
userMapper.selectById(1);
控制台:
SQL执行前
SELECT * FROM user WHERE id=1
SQL执行后
问题来了:
为什么 MyBatis 能在执行 SQL 前后插入逻辑?
MyBatis 插件本质是什么
答案就一句话:
插件本质就是 JDK 动态代理。
源码:
Plugin
打开:
public class Plugin implements InvocationHandler
看到这里其实已经破案了。
它本身就是一个动态代理处理器。
结构如下:
Mapper调用
↓
StatementHandler
↓
Plugin代理对象
↓
真实对象
所以插件并不是修改源码。
而是在外面套了一层代理。
插件是怎么创建出来的
启动 MyBatis 时:
Configuration
里面保存了所有插件:
protected final InterceptorChain interceptorChain
= new InterceptorChain();
注册插件:
configuration.addInterceptor(
new SqlLogInterceptor()
);
最终进入:
InterceptorChain
源码:
private final List<Interceptor> interceptors
= new ArrayList<>();
保存所有插件。
什么时候生成代理
关键源码:
InterceptorChain.pluginAll()
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
假设有三个插件:
分页插件
↓
日志插件
↓
权限插件
最终变成:
PermissionProxy
↓
LogProxy
↓
PageProxy
↓
Target
标准责任链模式。
plugin 方法干了什么
进入:
Interceptor
默认实现:
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
继续:
Plugin.wrap()
源码:
Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor)
)
是不是很熟悉?
这就是 JDK 动态代理。
相当于:
Proxy.newProxyInstance(...)
所以插件的核心根本不是什么黑科技。
就是代理模式。
SQL 执行时发生了什么
执行:
userMapper.selectById(1);
最终调用:
StatementHandler.prepare()
但此时拿到的已经不是原始对象。
而是:
Plugin Proxy
进入:
Plugin.invoke()
源码:
public Object invoke(
Object proxy,
Method method,
Object[] args)
这里开始判断:
if (匹配拦截方法) {
return interceptor.intercept(...);
}
匹配成功:
interceptor.intercept(...)
进入我们自己的代码:
public Object intercept(
Invocation invocation)
这里就能:
- 获取参数
- 修改参数
- 修改 SQL
- 修改结果
Invocation 是什么
很多人第一次写插件:
public Object intercept(
Invocation invocation)
都会疑惑:
这个对象哪里来的?
源码:
new Invocation(
target,
method,
args
)
内部保存:
private final Object target;
private final Method method;
private final Object[] args;
也就是说:
目标对象
执行方法
方法参数
全部给你了。
所以插件拥有极大的操作空间。
invocation.proceed() 干了什么
源码:
public Object proceed() throws Exception {
return method.invoke(target, args);
}
翻译成人话:
继续执行原方法
例如:
public Object intercept(
Invocation invocation)
throws Throwable {
System.out.println("before");
Object result =
invocation.proceed();
System.out.println("after");
return result;
}
执行顺序:
before
真实SQL执行
after
这其实就是典型的 AOP。
为什么分页插件能修改 SQL
以分页插件为例:
拦截:
StatementHandler.prepare()
拿到原始 SQL:
SELECT * FROM user
修改:
SELECT * FROM user
LIMIT 0,10
然后再执行:
invocation.proceed()
最终数据库执行的就是新 SQL。
所以分页插件并没有数据库魔法。
只是提前改写 SQL。
MyBatis 允许拦截哪些对象
官方只允许拦截四种核心对象:
Executor
负责执行 SQL
StatementHandler
负责 SQL 预处理
ParameterHandler
负责参数设置
ResultSetHandler
负责结果集处理
源码:
Plugin.getSignatureMap()
里面会校验。
如果不是这四种:
无法代理
Debug 一次完整流程
Mapper代理对象
↓
Executor
↓
StatementHandler
↓
Plugin.invoke()
↓
Interceptor.intercept()
↓
Invocation.proceed()
↓
真实SQL执行
↓
返回结果
看到这里你会发现:
MyBatis 插件机制和 Spring AOP 非常像。
都是:
代理
↓
拦截
↓
增强
↓
放行
区别只是:
Spring 拦截的是 Bean。
MyBatis 拦截的是 SQL 执行过程。
总结
MyBatis 插件看起来很神奇。
其实核心只有一句话:
利用 JDK 动态代理,
在 SQL 执行链路中插入自定义逻辑。
源码关键类:
Interceptor
Plugin
Invocation
InterceptorChain
执行流程:
InterceptorChain
↓
Plugin.wrap
↓
JDK动态代理
↓
Plugin.invoke
↓
Interceptor.intercept
↓
Invocation.proceed
↓
真实SQL执行
理解这一套之后:
- MyBatis-Plus 分页插件
- 多租户插件
- SQL审计插件
- 数据权限插件
基本都能看懂了。
上篇文章:
《MyBatis 动态 SQL 为什么能如此灵活?》
下篇文章:
《为什么很多公司禁用 MyBatis 二级缓存?》


