水煮MyBatis(二四)- 插件介绍和底层逻辑

136 阅读4分钟

前言

又来更新了,上半年就准备写这个知识点了,因为参与一个JAM活动而耽搁了一段的时间,久到忘记这个系列的存在。剩下的几个章节里,我会详细讲一下插件的写法,然后展开介绍一下我之前写过的两个插件。

插件是是什么

在mybatis的框架里,允许开发者对sql的执行过程,进行拦截增强处理,用来实现特殊业务逻辑。

插件接口

所有的自定义插件,都需要实现此接口,可以说,这就是插件的入口了。

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

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

  default void setProperties(Properties properties) {
    // NOP
  }

}

这个接口中的三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:

  • intercept:这个就是具体的拦截方法,在自定义插件中,在这个方法中,实现需要的业务逻辑。
  • plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般只有四个类型:Executor,ParameterHandler,ResultSetHandler,StatementHandler。
  • setProperties:这个方法用来传递插件的参数,在XML中,通过对指定插件属性设置,用以执行特定逻辑分支。

增强逻辑intercept

一般自定义插件只需要实现此接口,从Interceptor接口的定义可以看出端倪,其他两个方法都有默认实现,定义了default关键字。
根据拦截方法不同,可以从Invocation对象中获取原始参数

  • getTarget():获取被代理对象;
  • getMethod():获取被代理的方法;
  • getArgs():获取方法参数值;
  • proceed():执行方法,直接反射执行:method.invoke(target, args);

以分页插件的部分代码为例:

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        ResultHandler resultHandler = (ResultHandler) args[3];
        Executor executor = (Executor) invocation.getTarget();
        // ...
    }

核心方法plugin

从上面的方法介绍,就可以看出来,plugin方法扮演了非常重要的角色,参数里的target,是需要被代理的对象。在这个方法里,会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过JDK动态代理的方式,拦截并增强目标对象。

   public static Object wrap(Object target, Interceptor interceptor) {
        // 自定义插件里,指定的接口和方法
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        // 被代理对象类
        Class<?> type = target.getClass();
        // 从@Signature注解中,获取需要被代理的接口,逻辑是:如果注解中指定的类,是包含被代理对象,或者包含被代理对象的父类
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            // 返回动态代理对象,最终执行的不是原始的方法,而是此动态代理的方法
            return Proxy.newProxyInstance(
                    type.getClassLoader(),
                    interfaces,
                    new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }

注意一:
在getSignatureMap()方法里,通过@Signature注解里定义的方法名和参数,来获取唯一的方法。这就需要我们在定义注解的时候,注意方法名、参数类型和参数个数,才能找到需要被代理的方法。
Method method = signature.type().getMethod(signature.method(), signature.args())

注意二:
如果我们定义一个插件,指定其他类型,比如Object,会怎样呢?根据if条件来看的话,只会返回原始类型,不会进行任何增强处理。
if (interfaces.length > 0)

没有存在感的setProperties

在我后续的例子中,基本没有用到此特性,小透明实锤了。

<plugins>
    <plugin interceptor="XXX.XXX.plugin.XXXInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>

在XMLConfigBuilder类中,读取此配置:

  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        // 加载xml中定义的属性,赋予指定插件
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

相关注解

涉及到两个注解

  • @Intercepts:指定要拦截的目标方法;
  • @Signature:指定需要拦截的方法签名;

拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:

  • type: 拦截器需要拦截的接口,只能是下面列出的四个,至于为什么,看后续说明;
  • method: 拦截器所拦截接口中的方法,与type指定的接口匹配;
  • args: 拦截器所拦截方法的参数类型;

允许拦截的接口

  • Executor:执行器,提供操作数据库的接口;
  • ParameterHandler:参数处理器,设置sql的参数;
  • ResultSetHandler:结果集处理器,处理从数据库查询的结果集,封装成对象等;
  • StatementHandler:语法处理器,真正去执行数据库CRUD。

为什么是这四个接口

依我看来,用这四个接口可以完全诠释一个sql执行的生命周期,从语法定义到执行,再到结果生成。所以我们需要自定义的插件,全程围绕这四个接口开展。

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

  // 启动服务的时候,将自定义的插件加载到mybatis里的上下文环境
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

有人就会说了,看了这个类,也不理解为什么接口需要限定那四个啊!看下面这个截图,引用pluginAll方法的地方,指定了返回类型是Executor,ParameterHandler,ResultSetHandler,StatementHandler。

image.png

插件执行图例

假设我们在StatementHandler接口上定义了三个插件,那么selectXxx的执行逻辑如下:
image.png