Mybatis之插件分析

前言

Mybatis提供了强大的扩展功能,也就是Mybatis的插件(plugins)功能;MyBatis容许你在已映射语句执行过程当中的某一点进行拦截调用,拦截以后能够对已有方法添加一些定制化的功能,好比常见的分页功能;试图修改或重写已有方法的行为的时候,你极可能在破坏MyBatis 的核心模块,这些都是更低层的类和方法,因此使用插件的时候要特别小心。mysql

如何扩展

1.拦截点

拦截的点一共包含四个对象,若干方法,具体以下:git

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

有了拦截点以后,咱们须要告诉Mybatis在哪一个类下执行到哪一个方法的时候进行扩展;Mybatis提供了简单的配置便可实现;github

2.如何扩展

使用插件是很是简单的,只需实现Interceptor接口,并指定想要拦截的方法签名便可;以下面自定义的插件MyPlugin:sql

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class MyPlugin implements Interceptor {
    private Properties prop;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.err.println("====before===="+invocation.getTarget());
        Object obj = invocation.proceed();
        System.err.println("====after===="+invocation.getTarget());
        return obj;
    }
    
    @Override
    public Object plugin(Object target) {
        System.err.println("====调用生成代理对象====target:" + target);
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        this.prop = properties;
    }
}

经过注解的方式指定了方法的签名:类型、方法名、参数列表;若是要拦截多个方法能够配置多个@Signature经过逗号分隔;如上配置表示在执行到Executor的update和query方法时进行拦截,也就会执行Interceptor接口中定义的intercept方法,咱们能够简单的作一些修改,或者干脆在里面重写对应的方法,固然前提是对源码很熟悉的状况下;固然还须要Mybatis知道咱们自定义的MyPlugin,须要提供configuration配置:apache

<plugins>
        <plugin interceptor="com.mybatis.plugin.MyPlugin">
            <property name="dbType" value="mysql" />
        </plugin>
    </plugins>

作完以上配置以后,能够简单的作一个测试,执行Mybatis的一个查询功能,日志输出以下:mybatis

====调用生成代理对象====target:org.apache.ibatis.executor.CachingExecutor@31d7b7bf
====before====org.apache.ibatis.executor.CachingExecutor@31d7b7bf
====调用生成代理对象====target:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@f5ac9e4
====调用生成代理对象====target:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@7334aada
====调用生成代理对象====target:org.apache.ibatis.executor.statement.RoutingStatementHandler@4d9e68d0
====after====org.apache.ibatis.executor.CachingExecutor@31d7b7bf

生成的四个代理对象其实就是咱们上面介绍的四个拦截点,虽然都生成了代理对象,可是在代理对象执行的时候也会检查是否配置了指定拦截方法,因此执行Mybatis查询功能的时候,检查是否配置了query方法。app

插件分析

1.拦截器注册

咱们在configuration中经过标签plugins配置的插件,最后都会注册到一个拦截链InterceptorChain中,其中维护了拦截器列表:ide

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

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

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
}

Mybatis在解析plugins标签的时候,解析到每一个plugin的时候都会调用addInterceptor()方法将插件添加到interceptors,多个插件会在pluginAll()方法执行的时候依次被调用;工具

2.触发拦截器

过滤器链中提供了pluginAll()方法,此方法会在咱们上面介绍的四个类被实例化的时候调用,能够在开发工具中简单的使用快捷键查询此方法被调用的地方:
image.png
能够看一个最多见的Executor实例化:开发工具

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

以上首先根据不一样的executorType类型建立不一样的Executor,默认的如CachingExecutor,最后会调用interceptorChain的pluginAll方法,传入参数和返回参数都是Executor,能够简单理解就是经过pluginAll返回了Executor的代理类;

3.生成代理类

生成代理类的方式有:JDK自带的Proxy和CGlib方式,Mybatis提供了Plugin类提供了生成相关代理类的功能,因此在上面的实例MyPlugin的plugin方法中直接使用了Plugin.wrap(target, this),两个参数分别是:target对应就是上面的四种类型,this表明当前的自定义插件:

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

此方法首先获取自定义插件中配置的Signature,而后检查Signature中配置的类是不是当前target类型,若是匹配则经过JDK自带的Proxy建立代理类,不然直接返回target,不作任何代理处理;如上实例这里的Target是CachingExecutor,其对应的接口是Executor,而咱们在MyPlugin中的Signature中配置了Executor类,因此能够匹配成功;

4.触发执行

等到真正执行具体方法的时候,实际上是执行建立代理类时指定的InvocationHandler的invoke方法,能够发如今上节中指定的InvocationHandler是Plugin对象,Plugin自己也是继承于InvocationHandler,提供了invoke方法:

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

首先从signatureMap中获取拦截类对应的方法列表,而后检查当前执行的方法是否在要拦截的方法列表中,若是在则调用自定义的插件interceptor,不然执行默认的invoke操做;interceptor调用intercept方法的时候是传入的Invocation对象,其包含了三个参数分别是:target对应就是上面的四种类型,method当前执行的方法,args当前执行方法的参数;这里的方法名和参数是能够在源码里面查看的,好比Executor的query方法:

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

5.拦截处理

调用自定义拦截器的intercept方法,传入一个Invocation对象,若是只是记录一下日志,如上面的MyPlugin插件,这时候只须要执行proceed方法:

public class Invocation {
  private final Object target;
  private final Method method;
  private final Object[] args;

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

此方法其实就是默认的处理,和不使用代理类的状况产生的结果实际上是同样的,只不过代理类使用反射的方式调用;固然Mybatis的插件不止记录日志这么简单,好比咱们经常使用的插件PageHelper用来作物理分页等等;

总结

Mybatis提供了四个类,能够被用来拦截,用来进行功能扩展;本文介绍了如何使用插件进行功能扩展,而后从源码层面进行分析如何经过代理类来实现扩展。

示例代码

Github

相关文章
相关标签/搜索