SPI介面定義

定義了@SPI註解

public @interface SPI {

       Stringvalue() default ""; //指定預設的擴充套件點

只有在介面打了@SPI註解的介面類才會去查詢擴充套件點實現

會依次從這幾個檔案中讀取擴充套件點

META-INF/dubbo/internal/   //dubbo內部實現的各種擴充套件都放在了這個目錄了

META-INF/dubbo/

META-INF/services/

我們以Protocol介面為例, 介面上打上SPI註解,預設擴充套件點名字為dubbo

@SPI("dubbo")

public interface Protocol{

}

dubbo中內建實現了各種協議如:DubboProtocol InjvmProtocolHessianProtocol WebServiceProtocol等等


Dubbo預設rpc模組預設protocol實現DubboProtocol,key為dubbo



下面我們來細講ExtensionLoader類

1.      ExtensionLoader.getExtensionLoader(Protocol.class)

每個定義的spi的介面都會構建一個ExtensionLoader例項,儲存在

ConcurrentMap<Class<?>,ExtensionLoader<?>> EXTENSION_LOADERS 這個map物件中

2.      loadExtensionClasses 讀取擴充套件點中的實現類

a)       先讀取SPI註解的value值,有值作為預設擴充套件實現的key

b)       依次讀取路徑的檔案

META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol

META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol

META-INF/services/ com.alibaba.dubbo.rpc.Protocol

3.      loadFile逐行讀取com.alibaba.dubbo.rpc.Protocol檔案中的內容,每行內容以key/value形式儲存的。

a)     判斷類實現(如:DubboProtocol)上有米有打上@Adaptive註解,如果打上了註解,將此類作為Protocol協議的設配類快取起來,讀取下一行;否則適配類通過javasisit修改位元組碼生成,關於設配類功能作用後續介紹

如果類實現沒有打上@Adaptive, 判斷實現類是否存在入參為介面的構造器(就是DubbboProtocol類是否還有入參為Protocol的構造器),有的話作為包裝類快取到此ExtensionLoader的Set<Class<?>>集合中,這個其實是個裝飾模式

b)   如果類實現沒有打上@Adaptive, 判斷實現類是否存在入參為介面的構造器(就是DubbboProtocol類是否還有入參為Protocol的構造器),有的話作為包裝類快取到此ExtensionLoader的Set<Class<?>>集合中,這個其實是個裝飾模式


c)     如果即不是設配物件也不是wrapped的物件,那就是擴充套件點的具體實現物件

查詢實現類上有沒有打上@Activate註解,有快取到變數cachedActivates的map中

將實現類快取到cachedClasses中,以便於使用時獲取

4.      獲取或者建立設配物件getAdaptiveExtension

a)如果cachedAdaptiveClass有值,說明有且僅有一個實現類打了@Adaptive, 例項化這個物件返回

b) 如果cachedAdaptiveClass為空, 建立設配類位元組碼。

為什麼要建立設配類,一個介面多種實現,SPI機制也是如此,這是策略模式,但是我們在程式碼執行過程中選擇哪種具體的策略呢。Dubbo採用統一資料模式com.alibaba.dubbo.common.URL(它是dubbo定義的資料模型不是jdk的類),它會穿插於系統的整個執行過程,URL中定義的協議型別欄位protocol,會根據具體業務設定不同的協議。url.getProtocol()值可以是dubbo也是可以webservice, 可以是zookeeper也可以是redis。

設配類的作用是根據url.getProtocol()的值extName,去ExtensionLoader. getExtension( extName)選取具體的擴充套件點實現。

所以能夠利用javasist生成設配類的條件

1)介面方法中必須至少有一個方法打上了@Adaptive註解

2)打上了@Adaptive註解的方法引數必須有URL型別引數或者有引數中存在getURL()方法

下面給出createAdaptiveExtensionClassCode()方法生成javasist用來生成Protocol適配類後的程式碼

import com.alibaba.dubbo.common.extension;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {

//沒有打上@Adaptive的方法如果被調到拋異常
      public void destroy() {

throw new UnsupportedOperationException(
 "methodpublic abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interfacecom.alibaba.dubbo.rpc.Protocol is not adaptive method!")

}

//沒有打上@Adaptive的方法如果被調到拋異常
      public int getDefaultPort() {
             throw newUnsupportedOperationException(
             "method public abstractint com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interfacecom.alibaba.dubbo.rpc.Protocol is not adaptive method!");
      }

//介面中export方法打上@Adaptive註冊
      publiccom.alibaba.dubbo.rpc.Exporter export(
             com.alibaba.dubbo.rpc.Invokerarg0)  throws com.alibaba.dubbo.rpc.Invoker{
            if (arg0 == null)
                   throw newIllegalArgumentException("com.alibaba.dubbo.rpc.Invokerargument == null");
            //引數類中要有URL屬性

if(arg0.getUrl() == null)
                   throw newIllegalArgumentException( "com.alibaba.dubbo.rpc.Invokerargument getUrl() == null");
             //從入參獲取統一資料模型URL

com.alibaba.dubbo.common.URL url = arg0.getUrl();
           String extName =(url.getProtocol() == null ? "dubbo" : url.getProtocol());
           //從統一資料模型URL獲取協議,協議名就是spi擴充套件點實現類的key

if (extName == null) throw new IllegalStateException( "Fail to getextension(com.alibaba.dubbo.rpc.Protocol) name from url("  + url.toString() + ") usekeys([protocol])");
          

//利用dubbo服務查詢機制根據名稱找到具體的擴充套件點實現

com.alibaba.dubbo.rpc.Protocol extension =(com.alibaba.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);      

//調具體擴充套件點的方法

return extension.export(arg0);
 }

//介面中refer方法打上@Adaptive註冊
 publiccom.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
                    com.alibaba.dubbo.common.URLarg1) throws java.lang.Class {
     

//統一資料模型URL不能為空

if (arg1 == null)
            throw newIllegalArgumentException("url == null");
     

 com.alibaba.dubbo.common.URL url =arg1;

//從統一資料模型URL獲取協議,協議名就是spi擴充套件點實現類的key

String extName = (url.getProtocol() == null ?"dubbo" : url.getProtocol());
   if (extName == null)
      thrownewIllegalStateException("Failtogetextension(com.alibaba.dubbo.rpc.Protocol)name from url("+ url.toString() + ") use keys([protocol])");


   //利用dubbo服務查詢機制根據名稱找到具體的擴充套件點實現

com.alibaba.dubbo.rpc.Protocol extension =(com.alibaba.dubbo.rpc.Protocol)  ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
.getExtension(extName);
    //調具體擴充套件點的方法

return extension.refer(arg0, arg1);

}

}

5. 通過createAdaptiveExtensionClassCode()生成如上的java原始碼程式碼,要被java虛擬機器載入執行必須得編譯成位元組碼,dubbo提供兩種方式去執行程式碼的編譯1)利用JDK工具類編譯2)利用javassit根據原始碼生成位元組碼。

如上圖:

1)生成Adaptive程式碼code

2)利用dubbo的spi擴充套件機制獲取compiler的設配類

3)編譯生成的adaptive程式碼

在此順便介紹下 @Adaptive註解打在實現類上跟打在介面方法上的區別

1)如果有打在介面方法上,調ExtensionLoader.getAdaptiveExtension()獲取設配類,會先通過前面的過程生成java的原始碼,在通過編譯器編譯成class載入。但是Compiler的實現策略選擇也是通過ExtensionLoader.getAdaptiveExtension(),如果也通過編譯器編譯成class檔案那豈不是要死迴圈下去了嗎?

ExtensionLoader.getAdaptiveExtension(),對於有實現類上去打了註解@Adaptive的dubbo spi擴充套件機制,它獲取設配類不在通過前面過程生成設配類java原始碼,而是在讀取擴充套件檔案的時候遇到實現類打了註解@Adaptive就把這個類作為設配類快取在ExtensionLoader中,呼叫是直接返回

6.  自動Wrap上擴充套件點的Wrap類

這是一種裝飾模式的實現,在jdk的輸入輸出流實現中有很多這種設計,在於增強擴充套件點功能。這裡我們拿對於Protocol介面的擴充套件點實現作為例項講解。

如圖Protocol繼承關係ProtocolFilterWrapper, ProtocolListenerWrapper這個兩個類是裝飾物件用來增強其他擴充套件點實現的功能。ProtocolFilterWrapper功能主要是在refer 引用遠端服務的中透明的設定一系列的過濾器鏈用來記錄日誌,處理超時,許可權控制等等功能;ProtocolListenerWrapper在provider的exporter,unporter服務和consumer 的refer服務,destory呼叫時新增監聽器,dubbo提供了擴充套件但是沒有預設實現哪些監聽器。

Dubbo是如何自動的給擴充套件點wrap上裝飾物件的呢?

1)在ExtensionLoader.loadFile載入擴充套件點配置檔案的時候

對擴充套件點類有介面型別為引數的構造器就是包轉物件,快取到集合中去

2)在調ExtensionLoader的createExtension(name)根據擴充套件點key建立擴充套件的時候, 先例項化擴充套件點的實現, 在判斷時候有此擴充套件時候有包裝類快取,有的話利用包轉器增強這個擴充套件點實現的功能。如下圖是實現流程

7. IOC大家所熟知的ioc是spring的三大基礎功能之一, dubbo的ExtensionLoader在載入擴充套件實現的時候內部實現了個簡單的ioc機制來實現對擴充套件實現所依賴的引數的注入,         dubbo對擴充套件實現中公有的set方法且入參個數為一個的方法,嘗試從物件工廠ObjectFactory獲取值注入到擴充套件點實現中去。

      上圖程式碼應該不能理解,下面我們來看看ObjectFactory是如何根據型別和名字來獲取物件的,ObjectFactory也是基於dubbo的spi擴充套件機制的

它跟Compiler介面一樣設配類註解@Adaptive是打在類AdaptiveExtensionFactory上的不是通過javassist編譯生成的。

AdaptiveExtensionFactory持有所有ExtensionFactory物件的集合,dubbo內部預設實現的物件工廠是SpiExtensionFactory和SpringExtensionFactory,他們經過TreeMap排好序的查詢順序是優先先從SpiExtensionFactory獲取,如果返回空在從SpringExtensionFactory獲取。

1) SpiExtensionFactory工廠獲取要被注入的物件,就是要獲取dubbo spi擴充套件的實現,所以傳入的引數型別必須是介面型別並且介面上打上了@SPI註解,返回的是一個設配類物件。

2) SpringExtensionFactory,Dubbo利用spring的擴充套件機制跟spring做了很好的融合。在釋出或者去引用一個服務的時候,會把spring的容器新增到SpringExtensionFactory工廠集合中去, 當SpiExtensionFactory沒有獲取到物件的時候會遍歷SpringExtensionFactory中的spring容器來獲取要注入的物件

8. 下面 給出整體活動圖