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 二级缓存?》

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐