【框架原理】mybatis 插件机制

插件

根据功能,可以将插件分为两大类:

  • 第一类:插件是对系统的一种补充,例如在分布式系统中,可以使用插件的方式,实现内存插件、磁盘插件、线性网络插件、Paxos插件等。此类插件等同于组件

  • 第二类:插件是对系统默认功能的自定义修改,例如mybatis里面自定义插件,它实现的拦截器的功能。此类插件等同于拦截器

MyBatis拦截器插件

mybatis里面自定义插件属于拦截器插件,大凡拦截器(Interceptor)都需要关注两个要点:

  1. 拦截的对象是谁,用代码行话来说,target目标是谁?
  2. 拦截的技术方案是什么?

拦截的对象

我们知道,MyBatis有四大核心对象:

  1. ParameterHandler:处理SQL的参数对象
  2. ResultSetHandler:处理SQL的返回结果集
  3. StatementHandler:数据库的处理对象,用于执行SQL语句
  4. Executor:MyBatis的执行器,用于执行增删改查操作

那么,MyBatis拦截器针对的对象就是上面“四大金刚”。

拦截的技术方案

在Java里面,我们想拦截某个对象,只需要把这个对象包装一下,用代码行话来说,就是重新生成一个代理对象。

下面,我们将代理对象成为 “变身”、将原生对象称为 “原生”

也就是说,一旦配置上插件,ParameterHandler,ResultSetHandler,StatementHandler,Executor这四大核心对象,将会生成 “变身”,是一种代理对象,而不再是 “原身”。

MyBatis拦截器插件整个运行过程

没有插件的运行过程

有插件的运行过程

可以可以理解了吧?
一旦配置上插件,ParameterHandler,ResultSetHandler,StatementHandler,Executor这四大核心对象,将会生成 “变身”,是一种代理对象,而不再是 “原身”。

时序图

下面时序图更加清晰反映情况,

如下是时序图,在整个时序图中,涉及到mybatis插件部分已标红,基本上就是体现在上文中提到的四个类上,对这些类上的方法进行拦截。

运行过程详细的实现机制

实现分三步:插件配置信息的加载、代理对象的生成、拦截逻辑的执行

插件配置信息的加载

假设是基于xml配置,有如下配置

<plugins>
  <plugin interceptor="org.apache.ibatis.builder.ExamplePlugin">
    <property name="plubinProperty" value="100">
  </plugin>
</plugins>

那么通过XMLConfigBuilder解析。

  • XMLConfigBuilder首先是读取配置,读到 <plugins> 标签会进入循环。
  • 循环里面,为每一个 <plugin> 标签创建Interceptor对象实例 (比如上面的 ExamplePlugin
  • 设置Interceptor属性变量 (比如上面的 plubinProperty
  • 最后把Interceptor对象添加到 Configuration的InterceptorChain

(InterceptorChain是MyBatis提供的一个类,用来管理一堆Interceptor的)

这里需要和Spring的Interceptor做区分
除了实现原理上是一样的外,MyBatis的拦截器链和Spring的拦截器没有半毛钱关系

代理对象的生成

以Executor为例

我们知道,MyBatis是通过SqlSession实现数据库访问的。
而SqlSession是通过SqlSessionFactory创建的。

在SqlSessionFactory创建SqlSession实例的过程中,就调用了我们上面提到的Configuration类来创建Executor,进而将Executor放入SqlSession的构造方法中

而这个方法会调用上面提到的InterceptorChain的plugAll方法

而plugAll方法很简单,就是遍历拦截器链中的拦截器,调用拦截器的plugin方法对目标(本例的目标为Executor)进行封装。

最后把目标给返回

最后,拦截器(Interceptor)是个接口,而接口实现是根据业务情况决定的,因此原理部分就不需要继续了解每个接口的实现了。

代理对象的生成(总结)

可以总结为下面这幅图。

  1. SqlSessionFactory创建SqlSession期间,调用Configuration
  2. Configuration对InterceptorChain中的Interceptor进行遍历
  3. 遍历过程中,对符合条件的Executor进行封装,生成Executor代理对象

当然,除了创建SqlSession,还有其他情况会创建 Executor

至于其他的核心组件(ParameterHandler、ResultSetHandler、StatementHandle)
则是在Executor的被创建后,在不同阶段下被创建出来的

联系上面这幅图

至于代理的创建,原理是一样的,均是调用 configuration.pluginAll() 方法


拦截逻辑的执行

拦截逻辑的执行看源码最清晰

拦截逻辑相关的部分源码

首先,看Mybatis提供的拦截器有些什么方法

package org.apache.ibatis.plugin;

import java.util.Properties;

/**
 * MyBatis 拦截器接口源码
 * 
 * @author Clinton Begin
 */
public interface Interceptor {
/**
  * 这个方法是mybatis的核心方法
  * 要实现自定义逻辑,基本都是改造这个方法,即具体的业务逻辑编写在这里
  *
  * @param invocation 可以通过反射要获取原始方法和对应参数信息
  *        如:invocation.getTarget()可以获取对象原型
  * @return 代理方法执行后的返回值。
  * @throws Throwable 可能调用任何方法,因此可能抛出任意异常
  */
  Object intercept(Invocation invocation) throws Throwable;
/**
  * 作用是用来生成代理对象,使得被代理的对象一定会经过intercept方法
  * 通常都会使用mybatis提供的工具类Plugin来获取代理对象
  * 如:return Plugin.wrap(o, this);
  * 如果有自己独特需求,可以自定义
  *
  * @param o 被代理对象的原型。
  *          可能是:Executor、StatementHandler、ParemeterHandler、ResultHandler、甚至可以是前面四种类型的代理对象。
  * @return 对o进行封装后的代理对象,
  *          如果通过Plugin来构造代理对象:return Plugin.wrap(o, this);
  *          那么返回的是 Plugin 类型或者 原类型
  */
  Object plugin(Object target);
/**
  * 就是用来设置插件的一些属性
  *
  * @param properties 解析出来的配置信息
  */
  void setProperties(Properties properties);
}

由于真正去执行Executor、ParameterHandler、ResultSetHandler和StatementHandler类中的方法的对象是代理对象,而在执行方法时,首先调用的是Plugin类的invoke方法。因此我们还需要了解 Plugin类的运行原理。

下面是 Plugin 类的部分源码

// 继承java.lang.reflect.InvocationHandler
public class Plugin implements InvocationHandler {

  // 自定义的插件,在plugin()中通常调用这个wrap方法。
  // 这个wrap方法会返回Plugin类型对象,或者原target类型(当发现插件类型不匹配时)
  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) {
      // 当发现时匹配的类型时,返回Plugin类型封装的代理类
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  // 上面wrap方法,把代理对象封装为Plugin类型
  // 所以在执行代理类方法时,首先调用的是Plugin类(实现了InvocationHandler接口)的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)) {
        // 当发现是匹配的方法时,
        // 调用interceptor的自定义方法intercept,执行业务逻辑
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 否者调用原来的方法
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

拦截逻辑(总结)

  • 首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;

  • 判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即Interceptor接口的intercept方法实现),不需要则直接执行原方法。

这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:

自定义MyBatis拦截器插件

自定义拦截器定位到目标需要指明两点:

  • 拦截的对象
  • 拦截的方法(包括参数)

然后对于前面讲的三个流程实现,mybatis均提供了接口方法,让我们自定义。

  • 插件配置信息的加载
  • 代理对象的生成
  • 拦截逻辑的执行

于是,我们自定义mybatis拦截器插件,实质上就是需要把上面5个点给实现了。

指定拦截的对象、方法

可以拦截的对象也就那四大对象,每个对象有各自可以拦截的方法:

  • 执行器Executor(update、query、commit、rollback等方法);
  • 参数处理器ParameterHandler(getParameterObject、setParameters方法);
  • 结果集处理器ResultSetHandler(handleResultSets、handleOutputParameters等方法);
  • SQL语法构建器StatementHandler(prepare、parameterize、batch、update、query等方法);

每个方法还想需要指定对应的参数,来区分方法重载
具体每个对象有哪些方法,只需需要点到源码查找即可。
以Executor为例

至于每个方法的作用,可以通过SqlSession的实现上略知一二。
如:
DefaultSqlSession 有关于 executor.queryCursor的使用和注解

全面了解,则需要到网上找文章
如:
MyBatis 核心配置综述之Executor
mybatis四大接口之 Executor
(相对的,官网上找了很久,反而没找到此类介绍 😦)

指定插件的配置信息、生成方法、业务逻辑

直接以一个完整的代码作为例子吧

package com.vshop.veat.plugins;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Properties;

/**
 * @author alan smith
 * @version 1.0
 * @date 2020/4/13 19:55
 */
@Intercepts({
        // 拦截情况的签名。当情况符合,便进行拦截。
        @Signature(
                // 指定要拦截的类型,这里拦截执行器类型
                type = Executor.class,
                // 拦截执行器中更新的方法
                method = "query",
                args = {
                        MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class
                }
        )
})
@Slf4j
@Component
public class MyInterceptor implements Interceptor {

    /**
     * 这个方法是mybatis的核心方法
     * 要实现自定义逻辑,基本都是改造这个方法,即具体的业务逻辑编写在这里
     *
     * @param invocation 可以通过反射要获取原始方法和对应参数信息
     * @return 代理方法执行后的返回值。
     * @throws Throwable 可能调用任何方法,因此可能抛出任意异常
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("开始拦截....");
        Object result = invocation.proceed();
        log.info("结束拦截....");
        return result ;
    }

    /**
     * 作用是用来生成代理对象,使得被代理的对象一定会经过intercept方法
     * 通常都会使用mybatis提供的工具类Plugin来获取代理对象
     * 如:return Plugin.wrap(o, this);
     * 如果有自己独特需求,可以自定义
     *
     * @param o 被代理对象的原型。
     *          可能是:Executor、StatementHandler、ParemeterHandler、ResultHandler、甚至可以是前面四种类型的代理对象。
     * @return 对o进行封装后的代理对象,
     *          如果通过Plugin来构造代理对象:return Plugin.wrap(o, this);
     *          那么返回的是 Plugin 类型或者 原类型
     */
    @Override
    public Object plugin(Object o) {
        // 对目标对象进行包装,创建target对象的代理对象
        // 目的是将当前拦截器加入到对象中,就可以执行拦截器
        log.info("生成代理对象");
        return Plugin.wrap(o, this);
    }

    /**
     * 就是用来设置插件的一些属性
     *
     * @param properties 解析出来的配置信息
     */
    @Override
    public void setProperties(Properties properties) {
        //Interceptor.super.setProperties(properties);//NOP
        log.debug("插件配置的信息:{}", Arrays.toString(properties.stringPropertyNames().toArray()));
    }
}

打印的数据

2020-04-14 16:28:35.895 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.900 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 开始拦截....
2020-04-14 16:28:35.908 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.928 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.929 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 生成代理对象
2020-04-14 16:28:35.938 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - ==>  Preparing: SELECT product_category_id AS id,product_category_name AS name,product_category_type AS type,create_time,update_time FROM ve_product_category WHERE product_category_id=? 
2020-04-14 16:28:35.966 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - ==> Parameters: 1(Integer)
2020-04-14 16:28:36.036 [DEBUG] [main] com.vshop.veat.mapper.ProductCategoryMapper.selectById - <==      Total: 1
2020-04-14 16:28:36.036 [INFO ] [main] com.vshop.veat.plugins.MyInterceptor - 结束拦截....

小结

简单的说,mybatis插件就是对ParameterHandler、ResultSetHandler、StatementHandler、Executor这四个接口上的方法进行拦截,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好mybatis插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;

另外,如果配置了多个拦截器的话,会出现层层代理的情况,即代理对象代理了另外一个代理对象,形成一个代理链条,执行的时候,也是层层执行;

关于mybatis插件涉及到的设计模式和软件思想如下:

  1. 设计模式:代理模式、责任链模式;
  2. 软件思想:AOP编程思想,降低模块间的耦合度,使业务模块更加独立;

一些注意事项:

  1. 不要定义过多的插件,代理嵌套过多,执行方法的时候,比较耗性能;
  2. 拦截器实现类的intercept方法里最后不要忘了执行invocation.proceed()方法,否则多个拦截器情况下,执行链条会断掉;

参考:

posted @ 2020-04-14 16:35  lawsssscat  阅读(632)  评论(3编辑  收藏  举报