MyBatis是纸老虎吗?(四)

本文详细解析了MyBatis配置文件中的plugins元素,介绍了如何自定义插件、Interceptor接口的使用以及InterceptorChain在执行过程中的角色,展示了动态代理的创建过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

《MyBatis是纸老虎吗?(三)》这篇文章中我们一起梳理了MyBatis配置文件的解析流程,并详细介绍了其中的一些常见节点的解析步骤。通过梳理,我们弄清楚了MyBatis配置文件中的一些常用配置项与Java Bean之间的对应关系,这进一步加深了我们对MyBatis配置文件认识;通过梳理,我们对MyBatis的使用步骤有了更全面的了解,这进一步提高了我们使用MyBatis的能力。今天我想继续梳理MyBatis这个框架,因为我们了解的,仅仅是冰山的一角。MyBatis中还有很多其他实用的知识点和好的设计思想值得我们深究。那今天就一起研究一下MyBatis配置文件中的plugins元素吧。

1 plugins元素的定义及解析

上篇文章——《MyBatis是纸老虎吗?(三)》——有提到过这个元素。这个元素的作用就是允许开发者指定一个插件,这个插件可以在映射语句执行过程中的某一点进行拦截,然后做一些特殊的处理,比如数据分页、操作日志增强、sql性能监控等。那如何定义一个插件呢?很简单,只需实现org.apache.ibatis.plugin.Interceptor接口即可。下面是一个自定义插件的示例:

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

    private Properties properties = new Properties();

    public Object intercept(Invocation invocation) throws Throwable {
        // implement pre processing if need
        Object returnObject = invocation.proceed();
        // implement post processing if need
        return returnObject;
    }
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

}

这个拦截器中的intercept()方法并未做任何处理,只是调用了Invocation对象上的proceed()方法,并将该方法的执行结果返回给上级调用者。梳理到这里,我想看一下Interceptor接口的源码,如下所示:

public interface Interceptor {
  Object intercept(Invocation invocation) throws Throwable;
  // default xxxx,如果没有没记错的话,这是 jdk1.8的新特性
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  default void setProperties(Properties properties) {
    // NOP
  }
}

由此源码,我们可以知道Interceptor,拦截器,是一个接口,其中仅有一个名为intercept的方法,因此实现该接口的类一般都要对这个方法进行实现。上面展示的自定义拦截器就对这个方法进行了实现。注意:这个方法会接收一个Invocation类型的参数,该类的源码如下所示:

public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

梳理到这里,我非常想知道是这个自定义拦截器要怎样用。想必诸位都已先我一步知道了这个问题的答案:直接在MyBatis的配置文件config.xml中新增plugins配置项。具体代码如下所示:

<plugins>
    <plugin interceptor="包名.插件类名"></plugin>
</plugins>

进行到这里,所有的前期准备工作就完成了。下面就一起看一下这个元素的解析过程吧!通过上篇文章我们知道XMLConfigBuilder类的parse()方法开启了执行流程,其中执行解析工作的核心是与其同属一类的parseConfiguration()方法。该方法会对MyBatis配置文件中的元素按照既定顺序逐个解析。这些元素的解析顺序为:properties、settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers、mappers。上节我们一起梳理了properties、settings、typeAliases、environments四个元素的解析过程,这节就详细梳理一下plugins元素的解析过程。解析plugins元素的方法的名为pluginsElement(),其源码如下所示:

private void pluginsElement(XNode context) throws Exception {
  if (context != null) {
    for (XNode child : context.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor()
          .newInstance();
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

该方法被调用时的状态如下图所示(注意截图中context参数的内容,就是MyBatis配置文件中的plugins标签下的内容):

借助这段运行时状态图,让我们一起分析一下这段代码的执行逻辑:1.拿到context参数所代表的plugins元素下的所有plugin节点,并遍历这些节点,然后执行下述步骤;2.拿到plugin节点上interceptor属性的值,这个值就是我们自定义的拦截器的包全名+类名;3.解析plugin元素下的子元素,并将其包装为Properties对象;4.解析第二步拿到的数据,然后加载相应的类,并实例化一个对象出来(注意:这里用到了反射);5.将第三步解析出来的Properties对象设置到第四步创建的Interceptor对象上;6.将第四步创建的Interceptor对象设置到Configuration对象的interceptorChain属性上(这个操作是通过调用Configuration类上的addInterceptor()方法完成的。这里还有一点需要注意:interceptorChain的类型为InterceptorChain,这让我想到了责任链及Spring的AOP和事务,有兴趣的可以翻看一下我之前梳理的与这两个主题有关的系列文章《Spring AOP系列》《Spring事务系列》)。

关于上述解析步骤,个人觉得有以下几点需要注意:

  1. 上述第二步和第五步中都提到了Properties,为什么我们可以在plugin元素中使用property标签呢?为什么我们可以将这些值设置到Interceptor类型的对象上呢?这两个问题很好回答。关于第一个问题:因为MyBatis支持,如若不然,plugins元素的解析逻辑中不会出现Properties properties = child.getChildrenAsProperties()这样一行代码。那MyBatis是怎么支持的呢?这个就要看MyBatis配置文件的dtd约束文件了,先看下面这段从mybatis-3-config.dtd文件中摘抄出来的代码:<!ELEMENT plugin (property*)>。这段代码的大致意思就是说在plugin元素下可以有零个或多个property标签(具体参照下图“mybatis dtd文件关于plugin的定义”)。关于第二个问题:根据前面列出的源码,不难发现Interceptor源码中有一个default修饰的setProperties()方法,该方法返回值为void类型,默认不做任何处理。前面自定义的拦截器实现了这个方法。正因为Interceptor中有这样一个方法,所以解析代码中才有这样一句:interceptorInstance.setProperties(properties)。(也就是上面描述中的第五步)
  2. 上述第二步调用XMLConfigBuilder类的父类BaseBuilder类resolveClass()方法去解析我们在plugin元素中指定的interceptor属性值(包全名+拦截器名),该方法会继续调用BaseBuilder类中的resolveAlias()方法,这个方法会继续调用TypeAliasRegistry对象的resolveAlias()方法,这个方法的源码在上篇文章中已经展示过,这里就不再啰嗦,有兴趣可以翻看源码或者翻阅上篇文章。这段代码会直接将传递进来的string参数转为小写,然后从typeAliases中查找这个string参数代表的key是否存在,如果存在,则直接返回其对应的Class<?>类型的值,如果不存在,则直接使用Resources加载这个类。plugins的解析最终走的就是这一步

mybatis dtd文件关于plugin的定义

2 关于InterceptorChain的介绍

上小节的第六步中提到解析出来的Interceptor的对象会被设置到Configuration对象中的interceptorChain属性上。这个属性的实际类型为InterceptorChain。该类的源码为:

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();
  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);
  }
}

这个类中定义了一个List类型的变量interceptors,其持有的类型为Interceptor,所以第六步调用Configuration类上的addInterceptor()方法,最终实际上调用的就是这个类上的addInterceptor()方法向interceptors变量中添加数据。由此,我们可以得出这样的结论InterceptorChain类就是Interceptor的容器,其主要作用就是聚集所有实现了Interceptor接口的类,并充当调度的入口。那这个调度入口究竟在哪里呢?这个入口在《MyBatis是纸老虎吗?(二)》这篇文章中有提到。有兴趣的可以翻阅一下。如果实在不想翻,可以看下面这幅图:

通过这幅图片,我们可以看到在Configuration类的newExecutor()方法中会调用InterceptorChain类中的pluginAll()方法,同时会向该方法中传递一个Executor型的参数。这样我们就弄清楚InterceptorChain类使用的地方了。那这个pluginAll()方法究竟做了什么?继续跟踪源码,其运行时状态如下图所示:

注意:图片中target的实际类型为CachingExecutor,而interceptors中只有一个ExamplePlugin对象,所以循环体中interceptor.plugin(target)一句代码最终调用的是Interceptor接口中的default方法plugin(),这个方法的源码如下所示:

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

这个方法会调用Plugin类中名为wrap()的方法,然后将Executor(CachingExecutor)和Interceptor(ExamplePlugin)两个对象进行组合,Plugin#wrap()方法的源码如下所示:

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

这个方法会首先调用同类中的getSignatureMap()方法,该方法的主要作用是解析Interceptor实现类上的@Intercepts注解,然后解析该注解中的Signature属性,该注解及其属性的定义如下所示:

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

这里就不再就具体执行过程进行过多说明,详细逻辑请参看源码:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  // 解析Interceptors上的注解@Interceptors,这个注解的值是一个由Signature注解组成的数组
  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());
  }
  // 拿到Interceptors注解上的数据
  Signature[] sigs = interceptsAnnotation.value();
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
  // 遍历 Signature[] 数组,并对其进行处理。注意:Signature中有这样几个属性:type(Executor类型)、method(将要被拦截的方法名,这些方法都是type指定的类中的方法)、args(这个参数用于表示method参数指定的方法中包含的参数)
  // 比如上面示例中的配置:MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,它们是query方法的参数,这个方法位于Executor接口中
  for (Signature sig : sigs) {
    // 这里的MapUtil是ibatis自定义的一个工具类,这个方法的源码如下所示:
    // public static <K, V> V computeIfAbsent(Map<K, V> map, K key, Function<K, V> mappingFunction) {
    //     V value = map.get(key);
    //     if (value != null) {
    //         return value;
    //     }
    //     return map.computeIfAbsent(key, mappingFunction);
// }
// 最终这个方法会调用HashMap中的computeIfAbsent()方法,这个方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中
    // 最终就是将sig.type()和k->new HashSet<>()之间建立关系,并将这组关系存放到signatureMap中
    Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
    try {
      // 拿到Signature对象上的method值和args数据,然后拿到Signature中type表示的字节码,并从中查找method和args能够确定的方法,其结果是一个Method对象
      Method method = sig.type().getMethod(sig.method(), sig.args());
      // 将上一步创建的Method对象放到Set<Method>对象中
      methods.add(method);
    } catch (NoSuchMethodException e) {
      throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,
          e);
    }
  }
  // 返回signatureMap 对象(其中存储了在拦截器上的@Signatrue注解指定的数据,对应关系为:type->Method对象(method(args)))
  return signatureMap;
}

通过这个方法,我们会拿到一个Map<Class<?>, Set<Method>>集合,这个集合中存储的是一组映射:其中key就是@Signatrue注解中的type,value则是通过@Signature注解中的type、method和args确定的Method对象,也就是type所表示的类中的某个方法。这里我们一起看一下Intercepts及Signature的源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * Returns the java type.
   *
   * @return the java type
   */
  Class<?> type();

  /**
   * Returns the method name.
   *
   * @return the method name
   */
  String method();

  /**
   * Returns java types for method argument.
   *
   * @return java types for method argument
   */
  Class<?>[] args();
}

接着再来看Plugin#wrap()方法中的Class<?> type = target.getClass(),这里就是获取target对象的字节码数据,这里的target就是前面调用时传递进来的CachingExecutor对象紧接着再看Class<?>[] interfaces = getAllInterfaces(type, signatureMap)这句,其主要目的就是获取type上存在于signatureMap中的父接口,这里的getAllInterfaces()方法的源码如下所示:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  // 注意案例执行时,这里的type是CachingExecutor
  // 这里首先创建一个Set<Class<?>>型变量
  Set<Class<?>> interfaces = new HashSet<>();
  // 如果type值不为空,则进入 while 循环
// 调用type上的getInterfaces()方法获得type上的父接口,接着判断signatureMap是否包含这个字节码,如果包含就将其添加到interfaces集合中;如果不存在则继续遍历type上的父类
  while (type != null) {
    for (Class<?> c : type.getInterfaces()) {
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  return interfaces.toArray(new Class<?>[0]);
}

最后判断上一步获取的interfaces数组是否为空,如果不为空则直接调用Proxy的newProxyInstance()方法创建代理对象,这里是标准的jdk动态代理。调用该方法时,会传递三个参数:类加载器、字节码数组(interfaces)、InvocationHandler对象(实际就是Plugin对象,该对象有三个属性:target【案例中的CachingExecutor】、interceptor【由Executor.class组成的数组】、signatureMap【其中存储了在拦截器上的@Signatrue注解指定的数据,对应关系为:type->Method对象(method(args))】)。关于jdk动态代理可以参考《RMI 总结之代理》这篇文章。

至此系统就为我们创建了一个代理类,接着代码会向上返回,直至返回到Configuration类的newExecutor()方法中((Executor) interceptorChain.pluginAll(executor))。由此继续向上返回到DefaultSqlSessionFactory类的openSessionFromDataSource()方法中(Executor executor = configuration.newExecutor(tx, execType)),最后该方法会创建一个DefaultSqlSession对象并返回给上级调用者,即SpringTransactionApplication类的mybatis()中。

好,让我们回过头来看本小节开头的提出的那个问题:pluginAll()方法究竟做了什么?这个方法会在创建DefaultSessionFactory时调用,其主要作用是遍历MyBatis配置文件中指定的Interceptor集合,然后依次调用集合中的拦截器上的plugin()方法,对传递进来的Executor对象进行包装,即创建对应的代理对象。注意:这里的InterceptorChain类是责任链(Interceptor集合)的调度入口。

3 总结

记得在《MyBatis是纸老虎吗?(二)》结尾有提到过这样一个问题:这里的interceptorChain是为sql语句新增mysql查询条数的责任链吗?现在想想,这个问法有些问题:InterceptorChain是一个容器(暂时这么理解吧),用于存放实现了Interceptor接口的拦截器(通过InterceptorChain中的addInterceptor()方法完成拦截器的添加,数据会添加到InterceptorChain中的interceptors中其次该接口中的pluginAll()方法的主要作用是遍历Interceptor拦截器组成的集合(即:InterceptorChain中的interceptors属性),然后用集合中的元素对传递进该方法中的Object对象(实际上就是Executor对象,注意这里这么说是因为跟踪的代码在Configuration类的newExecutor()方法中)进行包装(通过调用Interceptor接口中的plugin()方法完成包装,这里的包装说白了就是创建动态代理对象,动态代理的创建过程可以看前一小节——关于InterceptorChain的介绍。说白了就是解析Interceptor实现类上的@Interceptors注解,然后用标准的jdk动态代理创建方式创建一个动态代理。其次问题中的为sql增加查询条数的说法不是在这一步完成的,这一步的主要作用是创建动态代理,比如创建Executor对象的动态代理对象。再次这里说是责任链个人觉得有点不妥,应该说是Interceptor链,即拦截器链。

下面就本篇的主要内容进行一个小结,本节主要目的是梳理一下如何在MyBatis中自定义并使用一个拦截器【期间梳理了拦截器的解析过程、又梳理了拦截器链的执行过程(即InterceptorChain中的pluginAll()方法的执行流程,通过这个梳理明白了动态代理的创建过程)】。下面就拦截器的定义步骤进行详细说明:

  1. 继承Interceptor接口,并实现其中的方法
  2. 在该实现类上添加@Intercepts注解,在该注解中指定@Signature数据,这里需要注意一下:@Intercepts用于标识该类是一个拦截器;@Signature(拦截点)则用于指明自定义拦截器需要拦截哪一个类型,哪一个方法,可以拦截的类型有:Executor——拦截执行器的方法、ParameterHandler——拦截参数的处理、ResultHandler——拦截结果集的处理、StatementHandler——拦截Sql语法构建的处理。@Signature中的属性有:type——上述四种类型中的一种;method——对应接口中的哪类方法(因为可能存在重载方法);args——对应哪一个方法的入参。
  3. 在MyBatis配置文件的plugins节点中指定相应的拦截器类
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器挖掘工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值