MyBatis 的执行过程中主要涉及四个很重要的接口,分别是 Executor、ParameterHandler、ResultSetHandler 和 StatementHandler,为了方便用户在上述接口执行过程中植入增强逻辑,MyBatis 实现了插件支持,即用户可以定义对上述接口的方法拦截逻辑,MyBatis 将通过动态代理将这里逻辑植入到具体接口方法的执行过程中。

MyBatis 官方对拦截器的使用介绍:https://mybatis.org/mybatis-3/configuration.html#plugins

默认地,Mybatis 允许拦截以下方法的调用:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

拦截器注册

下面通过实例跟踪源码看拦截器是如何注册到 Configuration 中的,假设有下面这一个拦截器

1
2
3
<plugins>
    <plugin interceptor="com.github.cszxyang.ibatis.test.interceptor.ExamplePlugin"/>
</plugins>

代码如下所示:

 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
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class, Object.class})})
@Slf4j
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    String interceptMethod = invocation.getMethod().getName();
    log.info("进入拦截器ExamplePlugin => interceptMethod: {}", interceptMethod);
    // 数据合法性校验
    if (invocation.getArgs() == null || invocation.getArgs().length < 1) {
      log.warn("ExamplePlugin:参数为空");
      return invocation.proceed();
    }
    Object arg1 = invocation.getArgs()[0];
    if (!(arg1 instanceof MappedStatement)) {
      log.error("ExamplePlugin:参数1不是MappedStatement对象");
      return invocation.proceed();
    }
    MappedStatement ms = (MappedStatement) arg1;
    log.info("ms: {}", ms.getSqlSource());
    Object param = null;
    if (invocation.getArgs().length > 1) {
      param = invocation.getArgs()[1];
    }
    log.info("args:{}, param:{}", invocation.getArgs(), param);
    BoundSql boundSql = ms.getBoundSql(param);
    log.info("boundSql:{}", boundSql);

    Object returnObject = invocation.proceed();
    return returnObject;
  }

  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

在解析 XML 文件过程中会解析 <plugin> 节点,具体的调用链是

org.apache.ibatis.session.SqlSessionFactoryBuilder#build
	-> org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
		-> org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
			-> org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement	

解析节点获取到拦截器的全限定类名和属性文件后,通过反射创建相应的拦截器对象,然后添加到 Configuration 中。

Snipaste_2021-11-15_16-22-25.png

Configuration 中维护了一个 InterceptorChain 对象,真正维护拦截器的是这个对象,其中通过链表 interceptors 收集所有的拦截器对象。

img

代理对象的生成

对于 Executor、ParameterHandler、ResultSetHandler、StatementHandler 的任何实现,在实例化对象时都会触发 InterceptorChain#pluginAll 方法,并将具体的接口实现传递进去。

Snipaste_2021-11-15_16-44-09.png

我们再回看 InterceptorChain#pluginAll 方法,它会遍历所有注册的拦截器,依次调用拦截器的 plugin 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// InterceptorChain#pluginAll
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}
// Interceptor#plugin
default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

而 Interceptor#plugin 方法中则调用 Plugin.wrap 方法,我们直接看 Plugin 类,它实现了 JDK 动态代理的 InvocationHandler 接口,在 wrap 方法中

  • 调用 getSignatureMap 方法:获取当前拦截器 Intercepts 注解里面标识的所有拦截的方法,比如,@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) 这句对应的返回结果是:<{Class@2337} "interface org.apache.ibatis.executor.Executor" -> {HashSet@2363} size = 1, Method@Update>,也就是一个键值对,key 是 Executor 接口,val 是其 update 方法。

    Snipaste_2021-11-15_16-55-31.png

  • Plugin#wrap 方法的入参 target 是 Executor、ParameterHandler、ResultSetHandler、StatementHandler 的任何实现,所以会获取到具体的接口定义,然后再去前面的 signatureMap 中找当前是拦截器是否对当前接口方法做拦截。

    • 没有的话,返回 target,也就是原对象本身,不做任何额外处理。
    • 否则,如果当前拦截器上对当前 target 有做拦截,那么将通过动态代理,为 target 生成一个 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
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);
        // issue #251
        if (interceptsAnnotation == null) {
            throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
        }
        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("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, 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]);
    }
}

如果一个类实现了目标的接口,那么调用完 Plugin#wrap 将会返回一个代理对象到 InterceptorChain#pluginAll 方法,再回看 InterceptorChain#pluginAll,其中藏了很多玄机。

1
2
3
4
5
6
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

假如我们定义了 3 个都是针对 Executor 的插件,那么上面代码将遍历这三个插件,分别创建三个代理对象,注意 target 一开始是 Executor 的实现的对象,后面创建完第一个代理对象后指向这个代理对象并传递进 interceptor.plugin 创建第二个代理对象,即第二、第三个的代理对象分别是基于第一个、第二个代理对象创建的代理对象,即 “代理的代理”,通过这种嵌套实现一条代理链,最后返回的 target 是针对 ExamplePlugin2 的代理对象,而 target.invoke 执行时会调用 Interceptor#intercept 方法,而 Interceptor#intercept 方法中会回调上一个代理对象的 invoke 方法,以此类推。

1
2
3
4
5
<plugins>
    <plugin interceptor="com.github.cszxyang.ibatis.test.interceptor.ExamplePlugin"/>
    <plugin interceptor="com.github.cszxyang.ibatis.test.interceptor.ExamplePlugin1"/>
    <plugin interceptor="com.github.cszxyang.ibatis.test.interceptor.ExamplePlugin2"/>
</plugins>

拦截器的触发

以一个被代理了的 Executor 对象为例,假设其具体实例是 CachingExecutor@485,被三个拦截器 ExamplePlugin、ExamplePlugin 和 ExamplePlugin2 拦截 update 方法,其代理链如下图所示,即 InterceptorChain#pluginAll 方法会依次创建三个代理对象 CachingExecutor@522、CachingExecutor@524 和 CachingExecutor@528。

  • 由于 CachingExecutor@485 已经被代理了,newExecutor 时最终得到的是代理对象 CachingExecutor@528,调用 CachingExecutor@528 的 update 方法时将会调用其绑定的调用处理器的 Plugin#invoke 方法,其中调用相应拦截器 ExamplePlugin2 的 intercepte 方法,然后调用 invocation.proceed(),其中调用 method.invoke(this.target, this.args),这里的 target 是当前代理对象绑定的下一个代理对象,即 CachingExecutor@524。

  • 接下来试图调用 CachingExecutor@524 的 update 方法,将会调用其绑定的调用处理器的 Plugin#invoke 方法,其中调用相应拦截器 ExamplePlugin1 的 intercepte 方法,然后调用 invocation.proceed(),其中调用 method.invoke(this.target, this.args),这里的 target 是当前代理对象绑定的下一个代理对象,即 CachingExecutor@522。

  • 接下来试图调用 CachingExecutor@522 的 update 方法,将会调用其绑定的调用处理器的 Plugin#invoke 方法,其中调用相应拦截器 ExamplePlugin 的 intercepte 方法,然后调用 invocation.proceed(),其中调用 method.invoke(this.target, this.args),这里的 target 是当前代理对象绑定的下一个代理对象,即 CachingExecutor@485。

  • 接下来试图调用 CachingExecutor@485 的 update 方法,由于 CachingExecutor@485 不是代理对象,所以它会执行相应的数据库交互操作。

Snipaste_2021-11-16_16-37-13.png