Contents

MyBatis Plugin工作机理

Contents

故事开头: 在使用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);
    }
  }

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

    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
 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都能正常工作。