源碼分析Mybatis插件(Plugin)機制與實戰(圖文並茂、提問式源碼閱讀技巧)

有了《Mybatis執行SQL的4大基礎組件詳解》《源碼解析MyBatis Sharding-Jdbc SQL語句執行流程詳解》兩篇文章的鋪墊,本文將直奔主題:Mybatis插件機制。java

>舒適提示:本文也是以提問式閱讀與探究源碼的技巧展現。spring

一、回顧

從前面的文章咱們已經知道,Mybatis在執行SQL語句的擴展點爲Executor、StatementHandler、ParameterHandler與ResultSetHandler,咱們本節將以Executor爲入口,向你們展現Mybatis插件的擴展機制。sql

咱們先來看回顧一下Mybatis Executor的建立入口。apache

1.1 Configuration#newExecutor

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);   // [@1](https://my.oschina.net/u/1198)
  return executor;
}

代碼@1,:使用InterceptorChain.pluginAll(executor)進行拆件化處理。數組

思考:使用該方法調用後,會返回一個什麼對象呢?如何自定義拆件,自定義插件如何執行呢?mybatis

那接下來咱們帶着上述疑問,從InterceptorChain類開始進行深刻學習。併發

二、InterceptorChain

從名字上看其大意爲攔截器鏈。app

2.1 類圖

在這裏插入圖片描述

  • InterceptorChain 攔截器鏈,其內部維護一個interceptors,表示攔截器鏈中全部的攔截器,並提供增長或獲取攔截器鏈的方法,下面會重點分析pluginAll方法。
  • Interceptor 攔截器接口,用戶自定義的攔截器須要實現該接口。
  • Invocation 攔截器執行時的上下文環境,其實就是目標方法的調用信息,包含目標對象、調用的方法信息、參數信息。其中包含一個很是重要的方法:proceed。
public Object proceed() throws InvocationTargetException, IllegalAccessException {
  return method.invoke(target, args);
}

該方法的主要目的就是進行處理鏈的傳播,執行完攔截器的方法後,最終須要調用目標方法的invoke方法。函數

記下來中先重點分析一下InterceptorChain方法的pluginAll方法,由於從開頭也知道,Mybatis在建立對象時,是調用該方法,完成目標對象的包裝。源碼分析

2.2 核心方法一覽

2.2.1 pluginAll

public Object pluginAll(Object target) {   // [@1](https://my.oschina.net/u/1198)
  for (Interceptor interceptor : interceptors) {   // @2
    target = interceptor.plugin(target);         
  // [@3](https://my.oschina.net/u/2648711)
  }
  return target;
}

代碼@1:目標對象,須要被代理的對象。

代碼@2:遍歷InterceptorChain的攔截器鏈,分別調用Intercpetor對象的Plugin進行攔截(@3)。

那接下來有三個疑問? 問題1:InterceptorChain中的interceptors是從何時初始化的呢,即攔截鏈中的攔截器從何而來。 問題2:從前面也得知,不管是建立Executor,仍是建立StatementHandler等,都是調用InterceptorChain#pluginAll方法,那是否是攔截器中的攔截器都會做用與目標對象,這應該是有問題的,該如何處理? 問題3:代理對象是如何建立的。

2.2.1 addInterceptor

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

要想知道interceptors是如何初始化的,咱們只須要查看該方法的調用鏈便可。

一路跟蹤到源頭,咱們會發如今初始化SqlSessionFactory時,會解析一個標籤plugin,就能夠得知,會在SqlSessionFacotry的一個屬性中配置全部的攔截器。 具體配置示例以下:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="shardingDataSource" />
    <property name="mapperLocations" value="classpath*:META-INF/mybatis/mappers/OrderMapper.xml" />

    <property name="plugins">
        <array>
            <bean id="teneantInteceptor" class="com.demo.inteceptor.TenaInteceptor"></bean>
        </array>
    </property>
</bean>

問題1已經解決。但後面兩個問題彷佛沒有什麼突破口。因爲目前所涉及的三個類,顯然不足以給咱們提供答案,咱們先將目光移到InterceptorChain所在包中的其餘類,看看其餘類的職責如何。

三、Intercepts與Signature

在org.apache.ibatis.plugin中存在以下兩個註解類:Intercepts與Signature,從字面意思就是用來配置攔截的方法信息。 在這裏插入圖片描述

  • Siganature註解的屬性說明以下:
    • Class<!--?--> type :須要攔截目標對象的類。
    • String method:須要攔截目標類的方法名。
    • Class<!--?-->[] args:須要攔截目標類的方法名的參數類型簽名。

備註:至於如何得知上述字段的含義,請看下文的Plugin#getSignatureMap方法。

但另一個類型Plugin類確引發了個人注意。接下來咱們將重點分析Plugin方法。

四、Plugin詳解

4.1 Plugin類圖

在這裏插入圖片描述

其中InvocationHandler爲JDK的動態代理機制中的事件執行器,咱們能夠隱約閾值代理對象的生成將基於JDK內置的動態代理機制。

Plugin的核心屬性以下:

  • Object target 目標對象。
  • Interceptor interceptor 攔截器對象。
  • Map<class<?>, Set< Method>> signatureMap 攔截器中的簽名映射。

4.2 構造函數

private Plugin(Object target, Interceptor interceptor, Map<class<?>, Set<method>&gt; signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

注意:其構造函數爲私有的,那如何構建Plugin呢,其構造方法爲Plugin的鏡頭方法wrap中被調用。

4.3 核心方法詳解

4.3.1 wrap

public static Object wrap(Object target, Interceptor interceptor) {
  Map<class<?>, Set<method>&gt; signatureMap = getSignatureMap(interceptor);  // @1
  Class<!--?--> type = target.getClass();   
  Class<!--?-->[] interfaces = getAllInterfaces(type, signatureMap);   // @2
  if (interfaces.length &gt; 0) {
    return Proxy.newProxyInstance(    // @3
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

代碼@1:獲取待包裝的Interceptor的方法簽名映射表,稍後詳細分析。

代碼@2:獲取須要代理的對象的Class上聲明的全部接口。

代碼@3:使用JDK內置的Proxy建立代理對象。Proxy建立代理對象的方法聲明以下:

public static Object newProxyInstance(ClassLoader loader,Class<!--?-->[] interfaces,InvocationHandler h),

注意其事件處理器爲Plugin,故在動態運行過程當中會執行Plugin的invoker方法。

在進入Plugin#invoker方法學習以前,咱們先重點查看一下getSignatureMap、getAllInterfaces的實現。

4.3.2 getSignatureMap

private static Map<class<?>, Set<method>&gt; getSignatureMap(Interceptor interceptor) {
  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);  // @1
  if (interceptsAnnotation == null) { // issue #251                                          // @2
    throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
  }
  Signature[] sigs = interceptsAnnotation.value();   // @3
  Map<class<?>, Set<method>&gt; signatureMap = new HashMap<class<?>, Set<method>&gt;(); 
  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;
}

代碼@1:首先從Interceptor的類上獲取Intercepts註解。

代碼@2:若是Interceptor的類上沒有定義Intercepts註解,則拋出異常,說明咱們在自定義插件時,必需要有Intercepts註解。

代碼@3:解析Interceptor的values屬性(Signature[])數組,而後存入HashMap<class<?>, Set< Method>>容器內。

>舒適提示:從這裏能夠得知:自定義的插件必須定義Intercepts註解,其註解的value值爲Signature。

4.3.3 getAllInterfaces

private static Class<!--?-->[] getAllInterfaces(Class<!--?--> type, Map<class<?>, Set<method>&gt; signatureMap) {
  Set<class<?>&gt; interfaces = new HashSet<class<?>&gt;();
  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()]);
}

該方法的實現比較簡單,並非獲取目標對象所實現的全部接口,而是返回須要攔截的方法所包括的接口。

4.3.4 invoke

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // @1
  try {
    Set<method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null &amp;&amp; methods.contains(method)) {   // @2
      return interceptor.intercept(new Invocation(target, method, args));   // @3
    }
    return method.invoke(target, args);                           // @4
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

代碼@1:首先對其參數列表作一個簡單的說明:

  • Object proxy 當前的代理對象
  • Method method 當前執行的方法
  • Object[] args 當前執行方法的參數

代碼@2:獲取當前執行方法所屬的類,並獲取須要被攔截的方法集合。

代碼@3:若是需被攔截的方法集合包含當前執行的方法,則執行攔截器的interceptor方法。

代碼@4:若是不是,則直接調用目標方法的Invoke方法。

從該方法能夠看出Interceptor接口的intercept方法就是攔截器自身須要實現的邏輯,其參數爲Invocation,在該方法的結束,須要調用invocation#proceed()方法,進行攔截器鏈的傳播。

從目前的學習中,咱們已經瞭解了Plugin.wrap方法就是生成帶來帶來類的惟一入口,那該方法在什麼地方調用呢?從代碼類庫中沒有找到該方法的調用鏈,說明該方法是供用戶調用的。

再看InterceptorChain方法的pluginAll方法:

public Object pluginAll(Object target) {   // @1
  for (Interceptor interceptor : interceptors) {   // @2
    target = interceptor.plugin(target);           // @3
  }
  return target;
}

該方法會遍歷用戶定義的插件實現類(Interceptor),並調用Interceptor的plugin方法,對target進行拆件化處理,即咱們在實現自定義的Interceptor方法時,在plugin中須要根據本身的邏輯,對目標對象進行包裝(代理),建立代理對象,那咱們就能夠在該方法中使用Plugin#wrap來建立代理類。

接下來咱們再來用序列圖來對上述源碼分析作一個總結: 在這裏插入圖片描述

看到這裏,你們是否對上面提出的3個問題都已經有了本身的答案了。

問題1:InterceptorChain中的interceptors是從何時初始化的呢,即攔截鏈中的攔截器從何而來。 答:在初始化SqlSesstionFactory的時候,會解析屬性plugins屬性,會加載全部的攔截器到InterceptorChain中。

問題2:從前面也得知,不管是建立Executor,仍是建立StatementHandler等,都是調用InterceptorChain#pluginAll方法,那是否是攔截器中的攔截器都會做用與目標對象,這應該是有問題的,該如何處理?

答案是在各自訂閱的Interceptor#plugin方法中,咱們能夠根據傳入的目標對象,是不是該攔截器關注的,若是不關注,則直接返回目標對象,若是關注,則使用Plugin#wrap方法建立代理對象。

問題3:代理對象是如何建立的? 代理對象是使用JDK的動態代理機制建立,使用Plugin#wrap方法建立。

五、實踐

實踐是檢驗真理的惟一標準,那到底如何使用Mybatis的插件機制呢? 建立自定義的攔截器Interceptor,實現Interceptor接口。

1)實現plugin方法,在該方法中決定是否須要建立代理對象,若是建立,使用Plugin#wrap方法建立。

2)實現interceptor方法,該方法中定義攔截器的邏輯,而且在最後請調用invocation.proceed()方法傳遞攔截器鏈。

3)使用Intercepts註解,定義須要攔截目標對象的方法簽名,支持多個。 將實現的Interceptor在定義SqlSessionFactory的配置中,放入plugins屬性。

最後給出一個Mybatis Plugin插件機制使用案例:基於Mycat+Mybatis的多租戶方案:基於Mybatis與Mycat的多租戶方式,經過Mybatis的插件機制,動態改寫SQL語句來實現多租戶


做者介紹:丁威,《RocketMQ技術內幕》做者,RocketMQ 社區佈道師,公衆號:中間件興趣圈 維護者,目前已陸續發表源碼分析Java集合、Java 併發包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源碼專欄。 </method></class<?></class<?></method></class<?></class<?></method></method></method></class<?></method></class<?></method></class<?></method></class<?></method></class<?></class<?>

相關文章
相關標籤/搜索