从一个问题初步了解MyBatis的一部分插件执行机制

前言

今天一位朋友在后端圈群里提了一个关于MyBatis插件的问题,正好有时间,就看了一下。

正文

问题大致是:我的工程里有两个MyBatis分页的插件,一个是PageHelper,另一个是自己写的一个分页插件,但是两个插件共存时,总是自己的插件不能执行到,是为什么?

PageHelper的插件:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package com.github.pagehelper;

import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.ExecutorUtil;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
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.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
* Mybatis - 通用分页拦截器
* <p>
* GitHub: https://github.com/pagehelper/Mybatis-PageHelper
* <p>
* Gitee : https://gitee.com/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
...
// 此处代码与本文无关省略
...

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
checkDialectExists();

List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if(dialect != null){
dialect.afterAll();
}
}
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

...
// 此处代码与本文无关省略
...

}

自己的插件:

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
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 org.springframework.stereotype.Component;

import java.util.Properties;

@Intercepts({@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
@Component
public class MyPageInterceptor implements Interceptor {

...
// 此处代码与本文无关省略
...

/**
* 拦截后要执行的方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 分页处理逻辑
return invocation.proceed();
}

/**
* 拦截器对应的封装原始对象的方法
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

...
// 此处代码与本文无关省略
...
}

关于这个问题我大致翻了一下MyBatis插件处的部分源码,最终大致明白了问题所在。

  1. MyBatis插件Plugin的被应用的入口点在这个地方:
    1
    org.apache.ibatis.plugin.InterceptorChain#pluginAll

可以看这个类完整的代码:(我加上了一点注释)

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
package org.apache.ibatis.plugin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* @author Clinton Begin
*/
public class InterceptorChain {

// 这里面是所有的拦截器
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}

public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}

}
  1. 上边的应用MyBatis插件(也就是拦截器)的过程实际上是会产生嵌套代理(JDK动态代理)的,类似于盗梦空间:一层梦境套着一层梦境
    1
    2
    3
    4
    5
    6
    // 以PageHelper为例,来看生成代理的部分逻辑:com.github.pagehelper.PageInterceptor#plugin

    @Override
    public Object plugin(Object target) {
    return Plugin.wrap(target, this);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 上边代码中的 Plugin.wrap(target, this);

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) {
// JDK动态代理的创建
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)); // 这里 Plugin 是 implements InvocationHandler 的
}
return target;
}

然后我们注意应用MyBatis插件逻辑中的这个循环

1
2
3
4
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;

这个循环中 interceptor.plugin(target) 可能会返回 上边代码中生成代理逻辑处所生成的target的代理,而代理又被赋值到了 target 本身再进行循环进入生成代理逻辑,这样就成了代理的代理(有可能数层)这样就形成了嵌套代理。

  1. 在正常情况下,我们自己的插件在 private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); 这个 interceptors 中的位置考前,而到了下边的生成代理的流程都结束后,最终的target,我们的插件拦截器就成了里层的代理,而PageHelper的代理在外层

我们的拦截器排位靠前,PageHelper的拦截器排位靠后

我们的代理在内层,PageHelper的代理在外层

  1. 在执行代理时,外层代理先于内层代理执行(具体执行逻辑我没有跟,我是在拦截器的拦截方法上加了断点判断出来的)

  2. 在执行分页插件所生成的代理(外层代理)时,在其拦截器方法中没有调用让拦截器链继续执行下一链条的关键方法 return invocation.proceed(); 因此内层代理永远也没机会执行到,这也就是为什么没执行我们自己插件的原因。

  3. 关于代理执行的顺序我们可以做一个实验,在第1步中应用所有代理处,我们使用idea的调试视图运行时修改变量的能力,将private final List<Interceptor> interceptors = new ArrayList<Interceptor>();拦截器列表反转一下,然后我们再观察,可以发现我们的插件先执行了,如果我们在插件的拦截方法中同样不写让拦截器链继续执行下一链条的关键方法 return invocation.proceed(); 那么PageHelper 的拦截器(此次为内层拦截器)就不会执行,而写了,则能执行。

手工调顺序

可以看到手工调序后我们的代理跑到了外层,而PageHelper的代理成了内层

PageHelper没有这一句代码,因此内层代理就都不执行了

到此问题基本解决,就着这个问题我大致了解了一部分MyBatis的内在逻辑,当然,代码看的比较仓促,很多细节尚不清晰,后续继续深入。

参考资料

Mybatis插件原理