故事开头:
在使用MyBatis的RowBounds时,发现结果没有和理论的一致,然后深入研究RowBounds的实现原理:MyBatis仅借助RowBounds在内存中完成了数据的分页处理——逻辑分页。还有一种是物理分页,就是常见的Limit offset, limit的方式(MySQL)。然后查资料,研究找到了很多的实现方式。我先尝试了一下其中的一种:使用Interceptor的方式。大体的逻辑是在当前执行的SQL后面把RowBounds的offset,limit按照limit offset,limit的方式拼接在SQL后面。
实现如下:
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
|
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
RowBounds rb = (RowBounds) args[2]; // RowBounds
if (null == rb || rb == RowBounds.DEFAULT) {
return invocation.proceed();
}
// append limit statement
StringBuilder sqlBuidler = new StringBuilder(boundSql.getSql());
String limit = String.format(" limit %d,%d", rb.getOffset(), rb.getLimit());
sqlBuidler.append(limit);
// replace sqlSource by reflection
SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sqlBuidler.toString(), boundSql.getParameterMappings());
Field field = MappedStatement.class.getDeclaredField("sqlSource");
field.setAccessible(true);
field.set(ms, sqlSource);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
|
别忘记在mybatis-config.xml中配置plugins:
1
2
3
|
<plugins>
<plugin interceptor="cn.zhucongqi.interceptor.PageInterceptor"/>
</plugins>
|
然后测试,成功了。
那么下面问题来了?这Interceptor是如何工作的,工作原理是什么? 然后就开始打断点跟踪如何实现的,然后一步步发现了MyBatis设计真的精妙得很哟。
正式进入正题,来说说Interceptor的工作原理。
Interceptor字面意思是拦截器,在很多得很有用应用。顾名思义,就是在do一件事之前先拦截一下,所以我们再做物理分页时,才有机会去干预,去拼接SQL。
从MyBatis-config.xml的配置文件来看,其属于MyBatis的Plugin范畴。
所以研究Plugin成为了重头戏。
从SqlSession入手,openSession时在DefaultSqlSessionFactory的私有方法**openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit)**中有这样一段代码
1
|
final Executor executor = configuration.newExecutor(tx, execType);
|
这段代码成了一个开头。再深入卡一下newExecutor的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 确保ExecutorType合法
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) { // 如果是Batch类型选择BatchExecutor
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) { // 如果是Reuse类型选择ReuseExecutor
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);// 否则是默认的SimpleExecutor
}
if (cacheEnabled) {
executor = new CachingExecutor(executor); // 二级缓存,用CachingExecutor包装一下
}
// 这里⬇️重要了
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
|
重点就在这里了executor = (Executor) interceptorChain.pluginAll(executor);
。
再看看这里做了什么?
1
2
3
4
5
6
|
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
|
好像又找到了一线索,这个的plugin方法和PageInterceptor中的plugin方法很像?看调用关系,果然就是它。
那么Plugin成了下一个线索。然后发现Plugin implements InvocationHandler,看来Proxy了。往下走,先来看看Plugin.wrap其实现:
1
2
3
4
5
6
7
8
9
10
11
12
|
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;
}
|
果然发现了Proxy.newProxyInstance
,这就是jdk动态代理了。那么这里的target就是interceptorChain.pluginAll(executor)中的executor了。那么对应type.getClassLoader()就是BatchExecutor或SimpleExecutor或ReuseExecutor或CachingExecutor的classloader了,那么在这里又动态创建了一个xxExecutor?继续往下找答案。
在这个wrap方法中,还有两个本地方法的调用一个是getSignatureMap,一个是getAllInterfaces。先看看代码:
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
|
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<?>[interfaces.size()]);
}
|
从代码实现来看,是根据Intercepts的注解来获取对应的Signature和Method信息,果然在PagerInterceptor的class上有这样的一个注解:
1
|
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
|
结合getSignatureMap和getAllInterfaces两个方法,看到目的是为了找到Executor.class的一个query方法,其定义是这样的:
1
|
query(MappedStatement ms, Object obj, RowBounds rowBounds, ResultHandler resultHandler);
|
原来越接近真相咯。这个方法在Executor接口中的确有对应的声明,如下:
1
|
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
|
其在各个Executor也有对应的实现。
解开真相前,再看看Plugin的invoke方法实现:
1
2
3
4
5
6
7
8
9
10
11
|
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);
}
}
|
那么真相就来咯。路径是这样的:
-
- openSession时,在DefaultSqlSessionFactory中根据executorType获取对应的Executor;
-
- 如果有配置Plugin,那么executor = (Executor) interceptorChain.pluginAll(executor);就会工作,会在创建好对应的Executor后,在使用Plugin的wrap方法wrap一下,其实就是获取其Intercepts注解的Signature,以此获取对应的Method;
-
- 根据Executor——target的type和对应的Interfaces,使用jdk的动态代理生成一个新的Executor;
-
- 动态生成了新的Executor,那么mapper调用时会触发它的任意方法时,都会触发对应的InnovationHandler也就Plugin的invoke方法;
-
- 在invoke方法中,根据对应的有@Intercepts注解的Interceptor的SignatureMap和当前调用的method来判断,将与Intercepts的Signature对应的方法调用进行拦截——调用Interceptor的intercept方法。
那么在PageInterceptor中,就是Executor调动query(MappedStatement ms, Object obj, RowBounds rowBounds, ResultHandler resultHandler)前就会先调用PageInterceptor的intercept方法,从而实现对操作的拦截。
搞定!!!
ref: https://pagehelper.github.io/docs/interceptor/
2019.12.10更新
在拦截相似方法,仅参数列表不一样的interceptor时,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<E> List<E> query(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler,
CacheKey cacheKey,
BoundSql boundSql) throws SQLException;
<E> List<E> query(
MappedStatement ms,
Object parameter,
RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException;
|
需要注意插件配置顺序:按照参数数量,倒序在MyBatis-config.xml中配置,这样能确保,每一个interceptor都能正常工作。因为对应不同的参数的方法,在拦截时,对应的调用顺序会被打乱,导致部分拦截无法工作。比如query拦截,直接调用了query的6参方法,那么对4参的方法拦截就不能生效——因为跳过了4参方法的调用。所以按照参数数量倒序配置,执行时会按照参数数量从少到多的顺序执行,保证每个interceptor都能正常工作。