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

相關文章
相關標籤/搜索