美文网首页
MyBatis之插件机制

MyBatis之插件机制

作者: 冰河winner | 来源:发表于2021-05-24 14:55 被阅读0次

1、概述

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

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

MyBatis拦截器针对的对象就是上面“四大金刚”。我们想拦截某个对象,需要把这个对象包装一下,也就是重新生成一个代理对象:

1.png

这样在每次调用Executor类的方法的时候,总是要经过Interceptor接口的拦截。

借助插件,MyBatis允许用户在SQL执行过程中的某一点进行拦截。常见的应用场景如下:

  • 分页查询:MyBatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用MyBatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;
  • 参数赋值:一般业务系统都会有创建者、创建时间、修改者、修改时间等字段,对于这四个字段的赋值可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;
  • 性能监控:对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;
  • 其它:基于插件机制,可以控制SQL执行的执行阶段、参数处理阶段、语法构建阶段以及结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。

配了插件的“四大金刚”会发生变身,这个变身的过程是这样的,以ParameterHandler为例:

第一步:根据插件配置,利用反射技术,创建interceptor拦截器

Interceptor interceptor = (Interceptor) MyInterceptor.class.newInstance();

第二步:创建原身

ParameterHandler parameterHandler = createParameterHandler();

第三步:原身+拦截器---->变身

parameterHandler = (ParameterHandler) Plugin.wrap(parameterHandler, interceptor);

在这一步,将parameterHandler和interceptor包装到一起,生成了变身,并重新赋值给parameterHandler变量。

没有插件的运行过程:

2.png

有插件的运行过程:

3.png

2、代理对象的生成

Mybatis插件主要是基于动态代理实现的,其中最为关键的就是代理对象的生成。

观察源码,发现这些可拦截的类对应的对象生成都是通过InterceptorChain的pluginAll方法来创建的,进一步观察pluginAll方法:

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

遍历所有拦截器,调用拦截器的plugin方法生成代理对象,注意生成代理对象重新赋值给target,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码。

编写拦截器的时候,一个典型的plugin方法实现方式如下:

@Override
public Object plugin(Object target) {
  return Plugin.wrap(target, this);
}

再进一步查看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;
}

典型的JDK动态代理实现,调用的是Proxy.newProxyInstance方法来生成代理对象。

以上逻辑对应的时序图如下,这里我们假设声明了两个拦截器,那么在创建target代理对象的时候,最终返回的代理对象proxy2,实际上代理了proxy1,而proxy1又代理了target:

4.png

3、拦截逻辑的执行

由于真正去执行Executor、ParameterHandler、ResultSetHandler和StatementHandler类中的方法的对象是代理对象,所以在执行方法时,首先调用的是Plugin类(实现了InvocationHandler接口)的invoke方法。

首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即Interceptor接口的intercept方法实现),不需要则直接执行原方法:

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

可以关注下Interceptor接口的intercept方法实现,一般需要用户自定义实现逻辑,其中有一个重要参数,即Invocation类,通过改参数我们可以获取执行对象,执行方法,以及执行方法上的参数,从而进行各种业务逻辑实现,一般在该方法的最后一句代码都是invocation.proceed()(内部执行method.invoke方法),否则将无法执行下一个拦截器的intercept方法。

以上逻辑对应的时序图如下,这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:

6.png

4、代码实例

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

Interceptor 接口的定义如下所示:

public interface Interceptor {

    /**
     * 拦截方法
     *
     * @param invocation 调用信息
     * @return 调用结果
     * @throws Throwable 若发生异常
     */
    Object intercept(Invocation invocation) throws Throwable;

    /**
     * 应用插件。如应用成功,则会创建目标对象的代理对象
     *
     * @param target 目标对象
     * @return 应用的结果对象,可以是代理对象,也可以是 target 对象,也可以是任意对象。具体的,看代码实现
     */
    Object plugin(Object target);

    /**
     * 设置拦截器属性
     *
     * @param properties 属性
     */
    void setProperties(Properties properties);

}

对于实现自己的Interceptor而言,有两个很重要的注解:

@Intercepts用于表明当前的对象是一个Interceptor。其值是一个@Signature数组。代码如下所示:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts
{
    Signature[] value();
}

@Signature则表明要拦截的接口、方法以及对应的参数类型。代码如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  Class<?> type();

  String method();

  Class<?>[] args();
}

下面来看一个自定义的简单Interceptor,出自MyBatis官方教程:

看一个自定义的简单Interceptor,出自MyBatis官方教程:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.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 Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

在上面的源码中Plugin.wrap(),是当前拦截器(ExamplePlugin)的代理类。MyBatis通过这个代理类来实现拦截的功能。

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

Mybatis在注册自定义的拦截器时,会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。然后这个插件将会拦截在 Executor 实例中所有的 “update” 方法调用,这里的 Executor 是负责执行低层映射语句的内部对象。

相关文章

网友评论

      本文标题:MyBatis之插件机制

      本文链接:https://www.haomeiwen.com/subject/dkuxsltx.html