【深刻淺出MyBatis系列六】插件原理

#0 系列目錄#java

MyBatis提供了一種插件(plugin)的功能,雖然叫作插件,但其實這是攔截器功能。那麼攔截器攔截MyBatis中的哪些內容呢?sql

MyBatis 容許你在已映射語句執行過程當中的某一點進行攔截調用。默認狀況下,MyBatis容許使用插件來攔截的方法調用包括:apache

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 攔截執行器的方法
  2. ParameterHandler (getParameterObject, setParameters) 攔截參數的處理
  3. ResultSetHandler (handleResultSets, handleOutputParameters) 攔截結果集的處理
  4. StatementHandler (prepare, parameterize, batch, update, query) 攔截Sql語法構建的處理

Mybatis採用責任鏈模式,經過動態代理組織多個攔截器(插件),經過這些攔截器能夠改變Mybatis的默認行爲(諸如SQL重寫之類的),因爲插件會深刻到Mybatis的核心,所以在編寫本身的插件前最好了解下它的原理,以便寫出安全高效的插件。數組

#1 攔截器的使用# ##1.1 攔截器介紹及配置## 首先咱們看下MyBatis攔截器的接口定義:緩存

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

比較簡單,只有3個方法。 MyBatis默認沒有一個攔截器接口的實現類,開發者們能夠實現符合本身需求的攔截器。下面的MyBatis官網的一個攔截器實例:安全

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}

全局xml配置:mybatis

<plugins>
    <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>

這個攔截器攔截Executor接口的update方法(其實也就是SqlSession的新增,刪除,修改操做),全部執行executor的update方法都會被該攔截器攔截到。 ##1.2 源碼分析## 首先從源頭->配置文件開始分析:app

  1. XMLConfigBuilder解析MyBatis全局配置文件的pluginElement私有方法
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
            interceptorInstance.setProperties(properties);
            configuration.addInterceptor(interceptorInstance);
        }
    }
}
  1. 具體的解析代碼其實比較簡單,就不貼了,主要就是經過反射實例化plugin節點中的interceptor屬性表示的類。而後調用全局配置類Configuration的addInterceptor方法。
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}
  1. 這個interceptorChain是Configuration的內部屬性,類型爲InterceptorChain,也就是一個攔截器鏈,咱們來看下它的定義:
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);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

}
  1. 如今咱們理解了攔截器配置的解析以及攔截器的歸屬,如今咱們回過頭看下爲什麼攔截器會攔截這些方法(Executor,ParameterHandler,ResultSetHandler,StatementHandler的部分方法):
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

以上4個方法都是Configuration的方法。這些方法在MyBatis的一個操做(新增,刪除,修改,查詢)中都會被執行到,執行的前後順序是Executor,ParameterHandler,ResultSetHandler,StatementHandler(其中ParameterHandler和ResultSetHandler的建立是在建立StatementHandler[3個可用的實現類CallableStatementHandler,PreparedStatementHandler,SimpleStatementHandler]的時候,其構造函數調用的[這3個實現類的構造函數其實都調用了父類BaseStatementHandler的構造函數])。ide

這4個方法實例化了對應的對象以後,都會調用interceptorChain的pluginAll方法,InterceptorChain的pluginAll剛纔已經介紹過了,就是遍歷全部的攔截器,而後調用各個攔截器的plugin方法。注意:攔截器的plugin方法的返回值會直接被賦值給原先的對象函數

因爲能夠攔截StatementHandler,這個接口主要處理sql語法的構建,所以好比分頁的功能,能夠用攔截器實現,只須要在攔截器的plugin方法中處理StatementHandler接口實現類中的sql便可,可以使用反射實現。

MyBatis還提供了@Intercepts和 @Signature關於攔截器的註解。官網的例子就是使用了這2個註解,還包括了Plugin類的使用:

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

#2 代理鏈的生成# Mybatis支持對Executor、StatementHandler、ParameterHandler和ResultSetHandler進行攔截,也就是說會對這4種對象進行代理。經過查看Configuration類的源代碼咱們能夠看到,每次都對目標對象進行代理鏈的生成。

下面以Executor爲例。Mybatis在建立Executor對象時會執行下面一行代碼:

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

InterceptorChain裏保存了全部的攔截器,它在mybatis初始化的時候建立。上面這句代碼的含義是調用攔截器鏈裏的每一個攔截器依次對executor進行plugin(插入?)代碼以下:

/** 
  * 每個攔截器對目標類都進行一次代理 
  * @param target 
  * @return 層層代理後的對象 
  */  
 public Object pluginAll(Object target) {  
     for(Interceptor interceptor : interceptors) {  
         target= interceptor.plugin(target);  
     }  
     return target;  
 }

下面以一個簡單的例子來看看這個plugin方法裏到底發生了什麼:

@Intercepts({@Signature(type = Executor.class, method ="update", args = {MappedStatement.class, Object.class})})  
public class ExamplePlugin implements Interceptor {  
    @Override  
    public Object intercept(Invocation invocation) throws Throwable {  
        return invocation.proceed();  
    }  
  
    @Override  
    public Object plugin(Object target) {  
        return Plugin.wrap(target, this);  
    }  
  
    @Override  
    public void setProperties(Properties properties) {  
    }
}

每個攔截器都必須實現上面的三個方法,其中:

  1. Object intercept(Invocation invocation)是實現攔截邏輯的地方內部要經過invocation.proceed()顯式地推動責任鏈前進,也就是調用下一個攔截器攔截目標方法。

  2. Object plugin(Object target)就是用當前這個攔截器生成對目標target的代理,實際是經過Plugin.wrap(target,this)來完成的,把目標target和攔截器this傳給了包裝函數。

  3. setProperties(Properties properties)用於設置額外的參數,參數配置在攔截器的Properties節點裏

註解裏描述的是指定攔截方法的簽名 [type,method,args] (即對哪一種對象的哪一種方法進行攔截),它在攔截前用於決斷

定義本身的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中咱們能夠決定是否要進行攔截進而決定要返回一個什麼樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。

對於plugin方法而言,其實Mybatis已經爲咱們提供了一個實現。Mybatis中有一個叫作Plugin的類,裏面有一個靜態方法wrap(Object target,Interceptor interceptor),經過該方法能夠決定要返回的對象是目標對象仍是對應的代理。這裏咱們先來看一下Plugin的源碼:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

//這個類是Mybatis攔截器的核心,你們能夠看到該類繼承了InvocationHandler
//又是JDK動態代理機制
public class Plugin implements InvocationHandler {

  //目標對象
  private Object target;
  //攔截器
  private Interceptor interceptor;
  //記錄須要被攔截的類與方法
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  //一個靜態方法,對一個目標對象進行包裝,生成代理類。
  public static Object wrap(Object target, Interceptor interceptor) {
    //首先根據interceptor上面定義的註解 獲取須要攔截的信息
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //目標對象的Class
    Class<?> type = target.getClass();
    //返回須要攔截的接口信息
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //若是長度爲>0 則返回代理類 不然不作處理
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  //代理對象每次調用的方法
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //經過method參數定義的類 去signatureMap當中查詢須要攔截的方法集合
      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)實現類上面的註解獲取相關信息
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //獲取註解信息
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    //爲空則拋出異常
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //得到Signature註解信息
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    //循環註解信息
    for (Signature sig : sigs) {
      //根據Signature註解定義的type信息去signatureMap當中查詢須要攔截方法的集合
      Set<Method> methods = signatureMap.get(sig.type());
      //第一次確定爲null 就建立一個並放入signatureMap
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //找到sig.type當中定義的方法 並加入到集合
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

  //根據對象類型與signatureMap獲取接口信息
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    //循環type類型的接口信息 若是該類型存在與signatureMap當中則加入到set當中去
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    //轉換爲數組返回
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

下面是倆個註解類的定義源碼:

package org.apache.ibatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  Signature[] value();
}
package org.apache.ibatis.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

  String method();

  Class<?>[] args();
}

#3 Plugin.wrap方法# 從前面能夠看出,每一個攔截器的plugin方法是經過調用Plugin.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) {  
        // 生成代理對象, Plugin對象爲該代理對象的InvocationHandler  (InvocationHandler屬於java代理的一個重要概念,不熟悉的請參考相關概念)  
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target,interceptor,signatureMap));  
    }  
    return target;  
}

這個Plugin類有三個屬性:

private Object target;// 被代理的目標類

private Interceptor interceptor;// 對應的攔截器

private Map<Class<?>, Set<Method>> signatureMap;// 攔截器攔截的方法緩存

getSignatureMap方法:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
}

**getSignatureMap方法解釋:**首先會拿到攔截器這個類的 @Interceptors註解,而後拿到這個註解的屬性 @Signature註解集合,而後遍歷這個集合,遍歷的時候拿出 @Signature註解的type屬性(Class類型),而後根據這個type獲得帶有method屬性和args屬性的Method。因爲 @Interceptors註解的 @Signature屬性是一個屬性,因此最終會返回一個以type爲key,value爲Set<Method>的Map。

@Intercepts({@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})})

好比這個 @Interceptors註解會返回一個key爲Executor,value爲集合(這個集合只有一個元素,也就是Method實例,這個Method實例就是Executor接口的update方法,且這個方法帶有MappedStatement和Object類型的參數)。這個Method實例是根據 @Signature的method和args屬性獲得的。若是args參數跟type類型的method方法對應不上,那麼將會拋出異常。

getAllInterfaces方法:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

**getAllInterfaces方法解釋:**根據目標實例target(這個target就是以前所說的MyBatis攔截器能夠攔截的類,Executor,ParameterHandler,ResultSetHandler,StatementHandler)和它的父類們,返回signatureMap中含有target實現的接口數組。

因此Plugin這個類的做用就是根據 @Interceptors註解,獲得這個註解的屬性 @Signature數組,而後根據每一個 @Signature註解的type,method,args屬性使用反射找到對應的Method。最終根據調用的target對象實現的接口決定是否返回一個代理對象替代原先的target對象。

咱們再次結合(Executor)interceptorChain.pluginAll(executor)這個語句來看,這個語句內部對executor執行了屢次plugin,第一次plugin後經過Plugin.wrap方法生成了第一個代理類,姑且就叫executorProxy1,這個代理類的target屬性是該executor對象。第二次plugin後經過Plugin.wrap方法生成了第二個代理類,姑且叫executorProxy2,這個代理類的target屬性是executorProxy1...這樣經過每一個代理類的target屬性就構成了一個代理鏈(從最後一個executorProxyN往前查找,經過target屬性能夠找到最原始的executor類)。

#4 代理鏈上的攔截# 代理鏈生成後,對原始目標的方法調用都轉移到代理者的invoke方法上來了。Plugin做爲InvocationHandler的實現類,他的invoke方法是怎麼樣的呢?

好比MyBatis官網的例子,當Configuration調用newExecutor方法的時候,因爲Executor接口的update(MappedStatement ms, Object parameter)方法被攔截器被截獲。所以最終返回的是一個代理類Plugin,而不是Executor。這樣調用方法的時候,若是是個代理類,那麼會執行:

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)) {  
           // 調用代理類所屬攔截器的intercept方法,  
           return interceptor.intercept(new Invocation(target, method, args));  
        }  
        return method.invoke(target, args);  
    } catch(Exception e) {  
        throw ExceptionUtil.unwrapThrowable(e);  
    }  
}

沒錯,若是找到對應的方法被代理以後,那麼會執行Interceptor接口的interceptor方法。

在invoke裏,若是方法簽名和攔截中的簽名一致,就調用攔截器的攔截方法。咱們看到傳遞給攔截器的是一個Invocation對象,這個對象是什麼樣子的,他的功能又是什麼呢?

public class Invocation {  
  
    private Object target;  
    private Method method;  
    private Object[] args;  
   
    public Invocation(Object target, Method method, Object[] args) {  
        this.target =target;  
        this.method =method;  
        this.args =args;  
    }  
    ...  
  
    public Object proceed() throws InvocationTargetException, IllegalAccessException {  
        return method.invoke(target, args);  
    }  
}

能夠看到,Invocation類保存了代理對象的目標類,執行的目標類方法以及傳遞給它的參數。

在每一個攔截器的intercept方法內,最後一個語句必定是return invocation.proceed()(不這麼作的話攔截器鏈就斷了,你的mybatis基本上就不能正常工做了)。invocation.proceed()只是簡單的調用了下target的對應方法,若是target仍是個代理,就又回到了上面的Plugin.invoke方法了。這樣就造成了攔截器的調用鏈推動。

public Object intercept(Invocation invocation) throws Throwable {  
    //完成代理類自己的邏輯  
    ...
    //經過invocation.proceed()方法完成調用鏈的推動
    return invocation.proceed();
}

#5 總結# MyBatis攔截器接口提供的3個方法中,plugin方法用於某些處理器(Handler)的構建過程。interceptor方法用於處理代理類的執行。setProperties方法用於攔截器屬性的設置。

其實MyBatis官網提供的使用 @Interceptors和 @Signature註解以及Plugin類這樣處理攔截器的方法,咱們不必定要直接這樣使用。咱們也能夠拋棄這3個類,直接在plugin方法內部根據target實例的類型作相應的操做。

整體來講MyBatis攔截器仍是很簡單的,攔截器自己不須要太多的知識點,可是學習攔截器須要對MyBatis中的各個接口很熟悉,由於攔截器涉及到了各個接口的知識點。

咱們假設在MyBatis配置了一個插件,在運行時會發生什麼?

  1. 全部可能被攔截的處理類都會生成一個代理
  2. 處理類代理在執行對應方法時,判斷要不要執行插件中的攔截方法
  3. 執行插接中的攔截方法後,推動目標的執行

若是有N個插件,就有N個代理,每一個代理都要執行上面的邏輯。這裏面的層層代理要屢次生成動態代理,是比較影響性能的。雖然能指定插件攔截的位置,但這個是在執行方法時動態判斷,初始化的時候就是簡單的把插件包裝到了全部能夠攔截的地方。

所以,在編寫插件時需注意如下幾個原則:

  1. 不編寫沒必要要的插件;
  2. 實現plugin方法時判斷一下目標類型,是本插件要攔截的對象才執行Plugin.wrap方法,否者直接返回目標本省,這樣能夠減小目標被代理的次數。
相關文章
相關標籤/搜索