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 中。
Configuration 中维护了一个 InterceptorChain 对象,真正维护拦截器的是这个对象,其中通过链表 interceptors 收集所有的拦截器对象。
代理对象的生成
对于 Executor、ParameterHandler、ResultSetHandler、StatementHandler 的任何实现,在实例化对象时都会触发 InterceptorChain#pluginAll 方法,并将具体的接口实现传递进去。
我们再回看 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 方法。
-
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 不是代理对象,所以它会执行相应的数据库交互操作。