MyBatis 源码解析——插件相关

在这篇文章我将介绍插件的源码,MyBatis插件是个很好的功能,类似于Spring的Aop,使得我们使用者可以较为灵活的在流程的特定点对SQL进行操作。

允许我引用网上的一段介绍,因为觉得总结得很好:MyBatis 允许我们以插件的形式对已映射语句执行过程中的某一点进行拦截调用,通俗一点来说,MyBatis 的插件其实更应该被称作为拦截器,总体上来说插件很好很好理解,因此这篇文章不会很长,所以为了扩充一下文章长度,最后将用Mybatis-Plus 的分页插件作为例子,讲解一下这个是如何切入的。

1. 基本使用

一般是这么使用:

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

MyBatis 允许插件拦截如下 4 个对象的方法。

  • Executor的 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 方法
  • ParameterHandler的 getParameterObject, setParameters 方法
  • ResultSetHandler的 handleResultSets, handleOutputParameters 方法
  • StatementHandler的 prepare, parameterize, batch, update, query 方法

如果想知道为啥是这几个,可以点开InterceptorChain的pluginAll方法,就可以发现是以上几个类调用了。我们可以发现InterceptorChain,里面有个interceptors维护了Interceptor的实现类list,因此当我们需要运用插件功能的时候,就需要新建一个Interceptor实现类并实现intercept方法,再将它声明成一个Bean,使得它可以添加进interceptors中,再在以上说的4个类相关的方法被调用时,生成代理类并调用。以上大致插件的基本使用和原理,那么让我们开始吧。

2. 从Executor开始

根据我们上面说的方法,我就拿Executor来举例了,那么点进newExecutor方法,也是我上篇涉及过方法

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  // 以上都在上篇解释过了,感兴趣的可以看看,
  // 在这调用插件,将executor作为参数。
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

让我们看看pluginAll。

public Object pluginAll(Object target) {
  // 遍历interceptors,包含我们新建的。
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

default Object plugin(Object target) {
  return Plugin.wrap(target, this);
}

pluginAll 很简单,只是遍历interceptors,并调用Interceptor的默认plugin方法,当然这只是一层皮,它又去调用Plugin的wrap方法。所以让我们看看Plugin的wrap方法。

public static Object wrap(Object target, Interceptor interceptor) {
  // key 为Signature中的type,即class,value为Signature中的method集合
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // 获取target的类,这个的target是指我们要代理的类,而不是我们写的继承自Interceptor的类,这里指的是Executor
  Class<?> type = target.getClass();
  // 获取目标类所有实现的接口, 并且它在signatureMap key中。
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // 通过Java的原生代理为目标类生成代理类。
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

2.1 获取Intercepts注解等信息

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  // 获取实现类上的Intercepts注解信息
  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注解数组
  Signature[] sigs = interceptsAnnotation.value();
  // Signature注解的上 类和方法的map
  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;
}

这个方法很好理解,就是获取signatureMap这个map。

再看下getAllInterfaces

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  Set<Class<?>> interfaces = new HashSet<>();
  while (type != null) {
    // 遍历代理类实现的所有接口,并遍历,如果signatureMap中包含接口名,那么添加至interfaces里面。
    for (Class<?> c : type.getInterfaces()) {
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  return interfaces.toArray(new Class<?>[interfaces.size()]);
}

这个方法也不难理解,至于为啥要使用这个方法,就拿我们用Executor来说,在newExecutor里也看见了,我们参数传递的是CachingExecutor,也就是Executor的实现类,但我们在最开始的TestExecutorInterceptor里声明的是Executor,所以这样就能避免本应该被代理的CachingExecutor没被代理到。

2.2 代理方法

Plugin实现了 InvocationHandler 接口,因此会调用 invoke 方法拦截被代理类所有方法的调用,那么看下invoke:

@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);
  }
}

invoke代码比较少,主要是判断拦截的方法是不是我们声明要切入的方法,是的话就会走我们自定义类下的intercept方法了。

此外,可以注意到实现Interceptor的类不止一个,InterceptorChain调用的也是pluginAll去遍历,所以最后生成的类其实是经过层层包裹过的,因此到时候就是从最外层开始慢慢向里调用。

3. 分析Mybatis-Plus分页插件源码

Mybatis-Plus是一款蛮好用的插件(此插件非彼插件),为啥不说很好用呢,因为我个人觉得他们在官网宣称的 只做增强不做改变这一点 有点意见,感觉并不像他们说的那样,而是有点强侵入的感觉了,当然这是我一家之言,而且它其他功能还是很方便的。

先看看官网的实例代码。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
    return interceptor;
}

官网的实例代码比较简单哈,就三行

  1. 新建一个MybatisPlusInterceptor
  2. 在其内部的list里加一个PaginationInnerInterceptor,这个类实现了InnerInterceptor
  3. 将这个MybatisPlusInterceptor 作为bean

@SuppressWarnings({"rawtypes"})
@Intercepts(
    {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @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}),
    }
)
// Executor的query、update,StatementHandler的prepare进行拦截
public class MybatisPlusInterceptor implements Interceptor {

  	// 内部维护了一个InnerInterceptor list。
    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
      	// 如果目标类是Executor
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
          	// 获取方法参数里的第二个,就是所有的
            Object parameter = args[1];
          	// 很明显Executor的update方法只有两个参数
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
          	// 如果是查询
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
              	// 如果调用的是只有四个参数的查询方法,获取BoundSql
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
                    boundSql = (BoundSql) args[5];
                }
              	// 遍历实现自InnerInterceptor的类
                for (InnerInterceptor query : interceptors) {
                  	// 判断是否能执行查询操作
                  	// PaginationInnerInterceptor重写了它,进行count,如果count为0就返回false,不会去执行SQL了
                  	// 虽然内部的逻辑还是去执行了SQL的,只是说
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                  	// 执行查询钱的操作
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
              	// 如果是更新
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
        return invocation.proceed();
    }
  
  // 省略了无关的方法
}

该说的都在注释里了,为啥我说它强侵入呢,就是因为它把我们的查询条件的参数全部封装成了 一个参数,这样,想在不修改现有业务的情况下,想对特定类的传入参数进行改变,就会有很大限制,当然也不是完全无计可施,只是说更麻烦了。

以上!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值