20 插件体系让 MyBatis 世界更加精彩

插件是应用程序中最常见的一种扩展方式,比如,在Chrome 浏览器上我们可以安装各种插件来增强浏览器自身的功能。在 Java 世界中,很多开源框架也使用了插件扩展方式,例如,Dubbo 通过 SPI 方式实现了插件化的效果,SkyWalking 依赖“微内核+插件”的架构轻松加载插件,实现扩展效果。

MyBatis 作为持久层框架中的佼佼者,也提供了类似的插件扩展机制。MyBatis 将插件单独分离出一个模块,位于 org.apache.ibatis.plugin 包中,在该模块中主要使用了两种设计模式:代理模式责任链模式

插件模块使用的代理模式是通过 JDK 动态代理实现的,代理模式的基础知识以及 JDK 动态代理的核心原理我们已经在前面《06 | 日志框架千千万,MyBatis 都能兼容的秘密是什么?》中介绍过了。下面我们就重点来看一下责任链模式的基础知识。

责任链模式

我们在写业务系统的时候,最常用的协议就是 HTTP 协议,最常用的 HTTP Server 是 Tomcat,所以这里我们就结合 Tomcat 处理 HTTP 请求的场景来说明责任链模式的核心思想。

HTTP 协议可简单分为请求头和请求体两部分,Tomcat 在收到一条完整的 HTTP 请求时,也会将其分为请求头和请求体两部分进行处理的。不过在真正的 Tomcat 实现中,会将 HTTP 请求细分为更多部分,然后逐步进行处理,整个 Tomcat 代码处理 HTTP 请求的实现也更为复杂。

试想一下,Tomcat 将处理请求的各个细节的实现代码都堆到一个类中,那这个类的代码会非常长,维护起来也非常痛苦,可以说是“牵一发而动全身”。如果 HTTP 请求升级,那就需要修改这个臃肿的类,显然是不符合“开放-封闭”原则的。

为了实现像 HTTP 这种多部分构成的协议的处理逻辑,我们可以使用责任链模式来划分协议中各个部分的处理逻辑,将那些臃肿实现类拆分成多个 Handler(或 Interceptor)处理器,在每个 Handler(或 Interceptor)处理器中只专注于 HTTP 协议中一部分数据的处理。我们可以开发多个 Handler 处理器,然后按照业务需求将多个 Handler 对象组合成一个链条,从而实现整个 HTTP 请求的处理。

这样做既可以将复杂、臃肿的逻辑拆分,便于维护,又能将不同的 Handler 处理器分配给不同的程序员开发,提高开发效率。

在责任链模式中,Handler 处理器会持有对下一个 Handler 处理器的引用,也就是说当一个 Handler 处理器完成对关注部分的处理之后,会将请求通过这个引用传递给下一个 Handler 处理器,如此往复,直到整个责任链中全部的 Handler 处理器完成处理。责任链模式的核心类图如下所示:

图片1.png

责任链模式核心类图

下面我们再从复用的角度看一下责任链模式带来的好处。

假设我们自定义了一套协议,其请求中包含 A、B、C 三个核心部分,业务系统使用 Handler A、Handler B、Handler C 三个处理器来处理这三部分的数据。如果业务变化导致我们的自定义协议也发生了变化,协议中的数据变成了 A、C、D 这三部分,那么我们只需要动态调整构成责任链的 Handler 处理器即可,最新的责任链变为 Handler A、Handler C、Handler D。如下图所示:

图片2.png

责任链示意图

由此可见,责任链模式可以帮助我们复用 Handler 处理器的实现逻辑,提高系统的可维护性和灵活性,很好地符合了“开放-封闭”原则。

Interceptor

介绍完责任链模式的基础知识之后,我们接着就来讲解MyBatis 中插件的相关内容。

MyBatis 插件模块中最核心的接口就是 Interceptor 接口,它是所有 MyBatis 插件必须要实现的接口,其核心定义如下:

public interface Interceptor {

  // 插件实现类中需要实现的拦截逻辑

  Object intercept(Invocation invocation) throws Throwable;

  // 在该方法中会决定是否触发intercept()方法

  default Object plugin(Object target) {

    return Plugin.wrap(target, this);

  }

  default void setProperties(Properties properties) {

    // 在整个MyBatis初始化过程中用来初始化该插件的方法

  }

}

MyBatis允许我们自定义 Interceptor 拦截 SQL 语句执行过程中的某些关键逻辑,允许拦截的方法有:Executor 类中的 update()、query()、flushStatements()、commit()、rollback()、getTransaction()、close()、isClosed()方法,ParameterHandler 中的 setParameters()、getParameterObject() 方法,ResultSetHandler中的 handleOutputParameters()、handleResultSets()方法,以及StatementHandler 中的parameterize()、prepare()、batch()、update()、query()方法。

通过本课程模块三的介绍我们知道,上述方法都是 MyBatis 执行 SQL 语句的核心组件,所以在使用自定义 Interceptor 拦截这些方法之前,我们需要非常了解 MyBatis 的核心原理以及 Interceptor 的拦截行为。

下面我们就结合一个 MyBatis 插件示例,介绍一下 MyBatis 中 Interceptor 接口的具体使用方式。这里我们首先定义一个DemoPlugin 类,定义如下:

@Intercepts({

        @Signature(type = Executor.class, method = "query", args = {

                MappedStatement.class, Object.class, RowBounds.class,

                ResultHandler.class}),

        @Signature(type = Executor.class, method = "close", args = {boolean.class})

})

public class DemoPlugin implements Interceptor {

    private int logLevel; 

    ... // 省略其他方法的实现

}

我们看到 DemoPlugin 这个示例类除了实现 Interceptor 接口外,还被标注了 @Intercepts 和 @Signature 两个注解。@Intercepts 注解中可以配置多个 @Signature 注解,@Signature 注解用来指定 DemoPlugin 插件实现类要拦截的目标方法信息,其中的 type 属性指定了要拦截的类,method 属性指定了要拦截的目标方法名称,args 属性指定了要拦截的目标方法的参数列表。通过 @Signature 注解中的这三个配置,DemoPlugin 就可以确定要拦截的目标方法的方法签名。在上面的示例中,DemoPlugin 会拦截 Executor 接口中的 query(MappedStatement, Object, RowBounds, ResultHandler) 方法和 close(boolean) 方法。

完成 DemoPlugin 实现类的编写之后,为了让 MyBatis 知道这个类的存在,我们要在 mybatis-config.xml 全局配置文件中对 DemoPlugin 进行配置,相关配置片段如下:

<plugins>

    <plugin interceptor="design.Interceptor.DemoPlugin">

        <!-- 对拦截器中的属性进行初始化 -->

        <property name="logLevel" value="1"/>

    </plugin>

</plugins>

通过前面《10 | 鸟瞰 MyBatis 初始化,把握 MyBatis 启动流程脉络(上)》对初始化流程的介绍我们知道,MyBatis 会在初始化流程中解析 mybatis-config.xml 全局配置文件,其中的 <plugin> 节点就会被处理成相应的 Interceptor 对象,同时调用 setProperties() 方法完成配置的初始化,最后MyBatis 会将 Interceptor 对象添加到Configuration.interceptorChain 这个全局的 Interceptor 列表中保存。

介绍完 Interceptor 的加载和初始化原理之后,我们再来看 Interceptor 是如何拦截目标类中的目标方法的。通过本课程模块三的介绍,我们知道 MyBatis 中 Executor、ParameterHandler、ResultSetHandler、StatementHandler 等与 SQL 执行相关的核心组件都是通过 Configuration.new*() 方法生成的。以 newExecutor() 方法为例,我们会看到下面这行代码,InterceptorChain.pluginAll() 方法会为目标对象(也就是这里的 Executor 对象)创建代理对象并返回。

executor = (Executor) interceptorChain.pluginAll(executor);

从名字就可以看出,InterceptorChain 是 Interceptor 构成的责任链,在其 interceptors 字段(ArrayList<Interceptor>类型)中维护了 MyBatis 初始化过程中加载到的全部 Interceptor 对象,在其 pluginAll() 方法中,会调用每个 Interceptor 的 plugin() 方法创建目标类的代理对象,核心实现如下:

public Object pluginAll(Object target) {

  for (Interceptor interceptor : interceptors) {

    // 遍历interceptors集合,调用每个Interceptor对象的plugin()方法

    target = interceptor.plugin(target);

  }

  return target;

}

Plugin

了解了 Interceptor 的加载流程和基本工作原理之后,我们再来介绍一下自定义 Interceptor 的实现。我们首先回到 DemoPlugin 这个示例,关注其中 plugin() 方法的实现:

@Override

public Object plugin(Object target) {

    // 依赖Plugin工具类创建代理对象

    return Plugin.wrap(target, this);

}

从 DemoPlugin 示例中,我们可以看到 plugin() 方法依赖 MyBatis 提供的 Plugin.wrap() 工具方法创建代理对象,这也是我们推荐的实现方式

MyBatis 提供的 Plugin 工具类实现了 JDK 动态代理中的 InvocationHandler 接口,同时维护了下面三个关键字段。

既然 Plugin 实现了 InvocationHandler 接口,我们自然需要关注其 invoke() 方法实现。在 invoke() 方法中,Plugin 会检查当前要执行的方法是否在 signatureMap 集合中,如果在其中的话,表示当前待执行的方法是我们要拦截的目标方法之一,也就会调用 intercept() 方法执行代理逻辑;如果未在其中的话,则表示当前方法不应被代理,直接执行当前的方法即可。下面就是 Plugin.invoke() 方法的核心实现:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    try {

        // 获取当前待执行方法所属的类

        Set<Method> methods = signatureMap.get(method.getDeclaringClass());

        // 如果当前方法需要被代理,则执行intercept()方法进行拦截处理

        if (methods != null && methods.contains(method)) {

            return interceptor.intercept(new Invocation(target, method, args));

        }

        // 如果当前方法不需要被代理,则调用target对象的相应方法

        return method.invoke(target, args);

    } catch (Exception e) {

        throw ExceptionUtil.unwrapThrowable(e);

    }

}

这里传入 Interceptor.intercept() 方法的是一个 Invocation 对象,其中封装了目标对象、目标方法以及目标方法的相关参数,在 DemoInterceptor.intercept() 方法实现中,就是通过调用 Invocation.proceed() 方法完成目标方法的执行。当然,我们自定义的 Interceptor 实现并不一定必须调用目标方法。这样,经过DemoInterceptor 的拦截之后,也就改变了 MyBatis 核心组件的行为。

最后,我们来看一下 Plugin 工具类对外提供的 wrap() 方法是如何创建 JDK 动态代理的。在 wrap() 方法中,Plugin 工具类会解析传入的 Interceptor 实现的 @Signature 注解信息,并与当前传入的目标对象类型进行匹配,只有在匹配的情况下,才会生成代理对象,否则直接返回目标对象。具体的代码实现以及注释说明如下所示:

public static Object wrap(Object target, Interceptor interceptor) {

    // 获取自定义Interceptor实现类上的@Signature注解信息,

    // 这里的getSignatureMap()方法会解析@Signature注解,得到要拦截的类以及要拦截的方法集合

    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);

    Class<?> type = target.getClass();

    // 检查当前传入的target对象是否为@Signature注解要拦截的类型,如果是的话,就

    // 使用JDK动态代理的方式创建代理对象

    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);

    if (interfaces.length > 0) {

        // 创建JDK动态代理

        return Proxy.newProxyInstance(

                type.getClassLoader(),

                interfaces,

                // 这里使用的InvocationHandler就是Plugin本身

                new Plugin(target, interceptor, signatureMap));

    }

    return target;

}

总结

这一讲我们重点介绍了 MyBatis 中插件模块的内容。