Qicz’s Thoughts HUB

The creative and technical writing. Do more, challenge more, know more, be more.

MyBatis Plugin工作机理

故事开头: 在使用MyBatis的RowBounds时,发现结果没有和理论的一致,然后深入研究RowBounds的实现原理:MyBatis仅借助RowBounds在内存中完成了数据的分页处理——逻辑分页。还有一种是物理分页,就是常见的Limit offset, limit的方式(MySQL)。然后查资料,研究找到了很多的实现方式。我先尝试了一下其中的一种:使用Interceptor的方式。大体的逻辑是在当前执行的SQL后面把RowBounds的offset,limit按照limit offset,limit的方式拼接在SQL后面。

实现如下:

 1@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
 2public class PageInterceptor implements Interceptor {
 3
 4    @Override
 5    public Object intercept(Invocation invocation) throws Throwable {
 6        Object[] args = invocation.getArgs();
 7        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
 8        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
 9        RowBounds rb = (RowBounds) args[2]; // RowBounds
10        if (null == rb || rb == RowBounds.DEFAULT) {
11            return invocation.proceed();
12        }
13
14        // append limit statement
15        StringBuilder sqlBuidler = new StringBuilder(boundSql.getSql());
16        String limit = String.format(" limit %d,%d", rb.getOffset(), rb.getLimit());
17        sqlBuidler.append(limit);
18
19        // replace sqlSource by reflection
20        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sqlBuidler.toString(), boundSql.getParameterMappings());
21        Field field = MappedStatement.class.getDeclaredField("sqlSource");
22        field.setAccessible(true);
23        field.set(ms, sqlSource);
24        return invocation.proceed();
25    }
26
27    @Override
28    public Object plugin(Object target) {
29        return Plugin.wrap(target, this);
30    }
31
32    @Override
33    public void setProperties(Properties properties) {
34
35    }
36}

别忘记在mybatis-config.xml中配置plugins:

1  <plugins>
2        <plugin interceptor="cn.zhucongqi.interceptor.PageInterceptor"/>
3  </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的实现

 1public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
 2	// 确保ExecutorType合法
 3    executorType = executorType == null ? defaultExecutorType : executorType;
 4    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
 5    Executor executor;
 6    if (ExecutorType.BATCH == executorType) { // 如果是Batch类型选择BatchExecutor
 7      executor = new BatchExecutor(this, transaction);
 8    } else if (ExecutorType.REUSE == executorType) { // 如果是Reuse类型选择ReuseExecutor
 9      executor = new ReuseExecutor(this, transaction);
10    } else {
11      executor = new SimpleExecutor(this, transaction);// 否则是默认的SimpleExecutor
12    }
13    if (cacheEnabled) {
14      executor = new CachingExecutor(executor); // 二级缓存,用CachingExecutor包装一下
15    }
16    // 这里⬇️重要了
17    executor = (Executor) interceptorChain.pluginAll(executor);
18    return executor;
19  }

重点就在这里了executor = (Executor) interceptorChain.pluginAll(executor);

再看看这里做了什么?

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

好像又找到了一线索,这个的plugin方法和PageInterceptor中的plugin方法很像?看调用关系,果然就是它。

那么Plugin成了下一个线索。然后发现Plugin implements InvocationHandler,看来Proxy了。往下走,先来看看Plugin.wrap其实现:

 1public static Object wrap(Object target, Interceptor interceptor) {
 2    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
 3    Class<?> type = target.getClass();
 4    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
 5    if (interfaces.length > 0) {
 6      return Proxy.newProxyInstance(
 7          type.getClassLoader(),
 8          interfaces,
 9          new Plugin(target, interceptor, signatureMap));
10    }
11    return target;
12  }

果然发现了Proxy.newProxyInstance,这就是jdk动态代理了。那么这里的target就是interceptorChain.pluginAll(executor)中的executor了。那么对应type.getClassLoader()就是BatchExecutor或SimpleExecutor或ReuseExecutor或CachingExecutor的classloader了,那么在这里又动态创建了一个xxExecutor?继续往下找答案。

在这个wrap方法中,还有两个本地方法的调用一个是getSignatureMap,一个是getAllInterfaces。先看看代码:

 1private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
 2    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
 3    // issue #251
 4    if (interceptsAnnotation == null) {
 5      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
 6    }
 7    Signature[] sigs = interceptsAnnotation.value();
 8    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
 9    for (Signature sig : sigs) {
10      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
11      try {
12        Method method = sig.type().getMethod(sig.method(), sig.args());
13        methods.add(method);
14      } catch (NoSuchMethodException e) {
15        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
16      }
17    }
18    return signatureMap;
19  }
20
21  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
22    Set<Class<?>> interfaces = new HashSet<>();
23    while (type != null) {
24      for (Class<?> c : type.getInterfaces()) {
25        if (signatureMap.containsKey(c)) {
26          interfaces.add(c);
27        }
28      }
29      type = type.getSuperclass();
30    }
31    return interfaces.toArray(new Class<?>[interfaces.size()]);
32  }

从代码实现来看,是根据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方法,其定义是这样的:

1query(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方法实现:

 1public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 2    try {
 3      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
 4      if (methods != null && methods.contains(method)) {
 5        return interceptor.intercept(new Invocation(target, method, args));
 6      }
 7      return method.invoke(target, args);
 8    } catch (Exception e) {
 9      throw ExceptionUtil.unwrapThrowable(e);
10    }
11  }

那么真相就来咯。路径是这样的:

    1. openSession时,在DefaultSqlSessionFactory中根据executorType获取对应的Executor;
    1. 如果有配置Plugin,那么executor = (Executor) interceptorChain.pluginAll(executor);就会工作,会在创建好对应的Executor后,在使用Plugin的wrap方法wrap一下,其实就是获取其Intercepts注解的Signature,以此获取对应的Method;
    1. 根据Executor——target的type和对应的Interfaces,使用jdk的动态代理生成一个新的Executor;
    1. 动态生成了新的Executor,那么mapper调用时会触发它的任意方法时,都会触发对应的InnovationHandler也就Plugin的invoke方法;
    1. 在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<E> List<E> query(
 2      MappedStatement ms,
 3      Object parameter,
 4      RowBounds rowBounds,
 5      ResultHandler resultHandler,
 6      CacheKey cacheKey,
 7      BoundSql boundSql) throws SQLException;
 8
 9<E> List<E> query(
10      MappedStatement ms,
11      Object parameter,
12      RowBounds rowBounds,
13      ResultHandler resultHandler) throws SQLException;

需要注意插件配置顺序:按照参数数量,倒序在MyBatis-config.xml中配置,这样能确保,每一个interceptor都能正常工作。因为对应不同的参数的方法,在拦截时,对应的调用顺序会被打乱,导致部分拦截无法工作。比如query拦截,直接调用了query的6参方法,那么对4参的方法拦截就不能生效——因为跳过了4参方法的调用。所以按照参数数量倒序配置,执行时会按照参数数量从少到多的顺序执行,保证每个interceptor都能正常工作。