Dubbo SPI 機制源碼分析(基於2.7.7)

Dubbo SPI 機制涉及到 @SPI@Adaptive@Activate 三個註解,ExtensionLoader 做爲 Dubbo SPI 機制的核心負責加載和管理擴展點及其實現。本文以 ExtensionLoader 的源碼做爲分析主線,進而引出三個註解的做用和工做機制。java

ExtensionLoader 被設計爲只能經過 getExtensionLoader(Class<T> type) 方法獲取到實例,參數 type 表示拿到的這個實例要負責加載的擴展點類型。爲了不在以後的源碼分析中產生困惑,請先記住這個結論:每一個 ExtensionLoader 只能加載其綁定的擴展點類型(即 type 的類型)的具體實現。也就是說,若是 type 的值是 Protocol.class,那麼這個 ExtensionLoader 的實例就只能加載 Protocol 接口的實現,不能去加載 Compiler 接口的實現。apache

怎麼獲取擴展實現

在 Dubbo 裏,若是一個接口標註了 @SPI 註解,那麼它就表示一個擴展點類型,這個接口的實現就是這個擴展點的實現。好比 Protocol 接口的聲明:數組

@SPI("dubbo")
public interface Protocol {}
複製代碼

一個擴展點可能存在多個實現,可使用 @SPI 註解的 value 屬性指定要選擇的默認實現。當用戶沒有明確指定要使用哪一個實現時,Dubbo 就會自動選擇這個默認實現。緩存

getExtension(String name) 方法能夠獲取指定名稱的擴展實現的實例,這個擴展實現的類型必須是當前 ExtensionLoader 綁定的擴展類型。這個方法會先查緩存裏是否有這個擴展實現的實例,若是沒有再經過 createExtension(String name) 方法建立實例。Dubbo 在這一塊設置了多層緩存,進入 createExtension(String name) 方法後又會調用 getExtensionClasses() 方法拿到當前 ExtensionLoader 已加載的全部擴展實現。若是還拿不到,那就調用 loadExtensionClasses() 方法真的去加載了。markdown

private Map<String, Class<?>> loadExtensionClasses() {
  // 取 @SPI 註解上的值(只容許存在一個值)保存到 cachedDefaultName
  cacheDefaultExtensionName();
  Map<String, Class<?>> extensionClasses = new HashMap<>();
  // 不一樣的策略表明不一樣的目錄,迭代進行加載
  for (LoadingStrategy strategy : strategies) {
    // loadDirectory(...)
    // 執行不一樣策略
  }
  return extensionClasses;
}
複製代碼

cacheDefaultExtensionName() 方法會從當前 ExtensionLoader 綁定的 type 上去獲取 @SPI 註解,並將其 value 值保存到 ExtensionLoader 的 cachedDefaultName 字段用來表示擴展點的默認擴展實現的名稱。app

SPI 配置的加載策略

接着迭代三種擴展實現加載策略。strategies 是經過 loadLoadingStrategies() 方法加載的,在這個方法裏已經對三種策略進行了優先級排序,排序規則是低優先級的策略放在前面。簡單看一下 LoadingStrategy 接口:框架

public interface LoadingStrategy extends Prioritized {
    String directory();
    default boolean preferExtensionClassLoader() {
        return false;
    }
    default String[] excludedPackages() {
        return null;
    }
    default boolean overridden() {
        return false;
    }
}
複製代碼

overridden() 方法表示當前策略加載的擴展實現是否能夠覆蓋比其優先級低的策略加載的擴展實現,優先級由 Prioritized 接口控制。爲了在加載擴展實現時可以方便的進行覆蓋操做,對加載策略進行預先排序就很是重要。這也是 loadLoadingStrategies() 方法要排序的緣由。ide

查找和解析 SPI 配置文件

loadDirectory() 方法在當前策略指定的目錄下查找 SPI 配置文件並加載爲 java.net.URL 對象,接下來 loadResource() 方法對配置文件進行逐行解析。Dubbo SPI 的配置文件是 key=value 形式,key 表示擴展實現的名稱,value 是擴展實現的具體類名,這裏直接 split 後對擴展實現進行加載,最後交給 loadClass() 方法處理。工具

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name, boolean overridden) throws NoSuchMethodException {
  if (!type.isAssignableFrom(clazz)) {
    throw new IllegalStateException("...");
  }
  // 適配類
  if (clazz.isAnnotationPresent(Adaptive.class)) {
    cacheAdaptiveClass(clazz, overridden);
  } else if (isWrapperClass(clazz)) { // 包裝類
    cacheWrapperClass(clazz);
  } else {
    clazz.getConstructor(); // 檢查點:擴展類必需要有一個無參構造器
    // 兜底策略:若是配置文件沒有按 key=value 這樣寫,就取類的簡單名稱做爲 key,即 name
    if (StringUtils.isEmpty(name)) {
      name = findAnnotationName(clazz);
      if (name.length() == 0) {
        throw new IllegalStateException("..." + resourceURL);
      }
    }

    String[] names = NAME_SEPARATOR.split(name);
    if (ArrayUtils.isNotEmpty(names)) {
      // 若是當前實現類標註了 @Activate 則緩存
      cacheActivateClass(clazz, names[0]);
      // 擴展實現能夠用逗號分隔取不少名字(a,b,c=com.xxx.Yyy),這裏迭代全部名字作緩存
      for (String n : names) {
        // 緩存 擴展實現的實例 -> 名稱
        cacheName(clazz, n);
        // 緩存 名稱 -> 擴展實現的實例
        saveInExtensionClass(extensionClasses, clazz, n, overridden);
      }
    }
  }
}
複製代碼

cacheAdaptiveClass() 方法是對 @Adaptive 的處理,這個稍後會介紹。源碼分析

包裝類

來看 isWrapperClass() 方法,這個方法用來判斷當前實例化的擴展實現是否爲包裝類。判斷條件很是簡單,只要某個類具備一個只有一個參數的構造器,且這個參數的類型和當前 ExtensionLoader 綁定的擴展類型一致,這個類就是包裝類

在 Dubbo 中包裝類都是以 Wrapper 結尾,好比 QosProtocolWrapper:

public class QosProtocolWrapper implements Protocol {
  private Protocol protocol;
	// 包裝類必要的構造器
  public QosProtocolWrapper(Protocol protocol) {
    if (protocol == null) {
      throw new IllegalArgumentException("protocol == null");
    }
    this.protocol = protocol;
  }
  
  @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) { // 一些額外的邏輯
            startQosServer(invoker.getUrl());
            return protocol.export(invoker);
        }
        return protocol.export(invoker);
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) { // 一些額外的邏輯
            startQosServer(url);
            return protocol.refer(type, url);
        }
        return protocol.refer(type, url);
    }
}
複製代碼

能夠看到,Dubbo 中的包裝類實際上就是 AOP 的一種實現,而且多個包裝類能夠不斷嵌套,相似 Java I/O 類庫的設計。回到 loadClass() 方法,若是當前是包裝類,則放入 cachedWrapperClasses 集合中保存。

兜底不標準的 SPI 配置文件

loadClass() 方法的最後一個 else 分支中,首先去獲取了一次當前擴展實現的無參構造器,由於以後實例化擴展實現的時候須要這個構造器,這裏等因而提早作了一個檢查。而後是作兜底操做,由於 SPI 配置文件可能沒有按照 Dubbo 的要求寫成 key=value 形式,那麼就把擴展實現類的類名做爲 keycacheActivateClass() 方法用於判斷當前擴展實現是否攜帶了 @Activate 註解,若是有則緩存,這個註解的用處後文會詳述。

擴展實現及其名稱的多種緩存

最後把擴展實現的名稱和擴展實現的 Class 對象進行雙向緩存。cacheName() 方法作 Class 對象到擴展實現名稱的映射,saveInExtensionClass() 是作擴展實現名稱到 Class 對象的映射。

saveInExtensionClass() 方法的參數 overridden 實際就是來自於加載策略 LoadingStrategy 的 overridden() 方法。上文提到過三個加載策略是在迭代時是按照優先級從小到大順序進行的,因此只要當前的 LoadingStrategy 容許覆蓋以前策略建立的擴展實現,那麼這裏 overridden 就爲 true

到了這裏實際上就是 loadExtensionClasses() 方法的所有執行邏輯,當方法執行完成後當前 ExtensionLoader 所綁定的擴展類型的全部實現類就所有被加載成了 Class 對象並放入了 cachedClasses 中。

實例化擴展實現

再往上返回到 createExtension(String name) 中,若是在已加載的擴展實現類裏找不到當前要獲取擴展實現則拋出異常。接着嘗試從緩存中獲取一下對應的實例,若是沒有則實例化並放入緩存。injectExtension() 方法就是經過反射將當前實例化出來的擴展實現所依賴的其餘擴展實現也初始化並賦值。

這裏用到一個 ExtensionFactory objectFactory,AdaptiveExtensionFactory 做爲 ExtensionFactory 的適配實現,對 SpiExtensionFactory 和 SpringExtensionFactory 進行了適配。當要獲取一個擴展實現時,都是調用 AdaptiveExtensionFactory 的 getExtension(Class<T> type, String name) 方法。

public <T> T getExtension(Class<T> type, String name) {
  for (ExtensionFactory factory : factories) {
    T extension = factory.getExtension(type, name);
    if (extension != null) {
      return extension;
    }
  }
  return null;
}
複製代碼

這個方法分別嘗試調用兩個具體實現的 getExtension() 方法來獲取擴展實現。SpiExtensionFactory 是從 Dubbo 本身的容器裏查找擴展實現,實際就是調用 ExtensionLoader 的方法來實現,算是一個門面。SpringExtensionFactory 顧名思義就是從 Spring 容器內查找擴展實現,畢竟不少時候 Dubbo 都是配合着 Spring 在使用。

回到 createExtension(String name) 方法繼續往下看,接下來是迭代在加載擴展實現時保存的包裝類,滾動將上一個包裝完的實例做爲下一個包裝類的構造器參數進行包裝,也就是說最終拿到的擴展實現的實例是最後一個包裝類的實例。最後的最後,若是擴展實現有 Lifecycle 接口,則調用其 initialize() 方法初始化生命週期。至此,一個擴展實現就被建立出來了!

怎麼選擇要使用的擴展實現

loadClass() 方法中提到過,若是加載的擴展實現帶有 @Adaptive 註解,cacheAdaptiveClass() 方法將會把這個擴展實現按照加載策略的覆蓋(overridden)設置賦值給 cachedAdaptiveClass

@Adaptive 的做用

Dubbo 中的擴展點通常都具備不少個擴展實現,簡單說就是一個接口存在不少個實現。但接口是不能被實例化的,因此要在運行時找一個具體的實現類來實例化。 @Adaptive 是用來在運行時決定選擇哪一個實現的。若是標註在類上就表示這個類是適配類,加載擴展實現的時候直接賦值給 ExtensionLoader 的 cachedAdaptiveClass 字段便可,例如上文講到的 AdaptiveExtensionFactory。

因此這裏簡單總結一下,所謂適配類就是在實際使用擴展點的時候用來選擇具體的擴展實現的那個類

@Adaptive 也能夠標註在接口方法上,表示這個方法要在運行時經過字節碼生成工具動態生成方法體,在方法體內選擇具體的實現來使用,好比 Protocol 接口:

@SPI("dubbo")
public interface Protocol {
  @Adaptive
  <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
 	@Adaptive
  <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
複製代碼

很明顯,Protocol 的每一個實現都有本身暴露服務和引用服務的邏輯,若是直接根據 URL 去解析要使用的協議並實例化顯然不是一個好的選擇。做爲一個 Spring 應用工程師,應該馬上想到 IoC 纔是人間正道。Dubbo 的開發者(可能)也是這麼想的,可是本身搞一套 IoC 出來又好像不是太合適,因而就經過了字節碼加強的方式來實現。

動態適配類的建立

若是一個擴展點的全部實現類上都沒有攜帶 @Adaptive 註解,可是擴展點的某些方法上帶了 @Adaptive 註解,這就表示 Dubbo 須要在運行時使用字節碼加強工具動態的建立一個擴展點的代理類,在代理類的同名方法裏選擇具體的擴展實現進行調用。

這麼說有點抽象,咱們來看 ExtensionLoader 的 getAdaptiveExtension() 方法。這個方法獲取當前 ExtensionLoader 綁定的擴展點的適配類,首先從 cachedAdaptiveInstance 上嘗試獲取,這個字段保存的是上文提到的 cachedAdaptiveClass 實例化的結果。若是獲取不到,通過雙重檢查鎖後調用 createAdaptiveExtension() 方法進行適配類的建立。

createAdaptiveExtension() 方法又調用 getAdaptiveExtensionClass() 方法拿到適配類的 Class 對象,即上文提到的 cachedAdaptiveClass,而後將 Class 實例化後調用 injectExtension() 方法進行注入。

getAdaptiveExtensionClass() 方法發現 cachedAdaptiveClass 沒有值後轉而調用 createAdaptiveExtensionClass() 方法動態生成一個適配類。這裏涉及到的幾個方法很簡單就不貼代碼了,下面看一下動態生成適配的方法。

private Class<?> createAdaptiveExtensionClass() {
  String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
  ClassLoader classLoader = findClassLoader();
  org.apache.dubbo.common.compiler.Compiler compiler = 
    ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class)
    .getAdaptiveExtension();
  return compiler.compile(code, classLoader);
}
複製代碼

首先調用 AdaptiveClassCodeGenerator 類的 generate() 方法把適配類生成好,而後也是走 SPI 機制拿到須要的 Compiler 的適配類執行編譯,最後把編譯出來的適配類的 Class 對象返回。

Dubbo 使用 javassist 框架來動態生成適配類,AdaptiveClassCodeGenerator 類的 generate() 方法實際就是作的適配類文件的字符串拼接。具體的生成邏輯沒有什麼好講的,都是些字符串操做,這裏簡單寫個示例:

@SPI
interface TroubleMaker {
    @Adaptive
    Server bind(arg0, arg1);
  
    Result doSomething();
}
public class TroubleMaker$Adaptive implements TroubleMaker {
  
  	public Result doSomething() {
      throw new UnsupportedOperationException("The method doSomething of interface TroubleMaker is not adaptive method!");
    }
  
    public Server bind(arg0, arg1) {
        TroubleMaker extension =
            (TroubleMaker) ExtensionLoader
                .getExtensionLoader(TroubleMaker.class)
                .getExtension(extName);
        return extension.bind(arg0, arg1);
    }
}
複製代碼

假設有個擴展點叫 TroubleMaker,那麼動態生成的適配類就叫作 TroubleMaker$Adaptive,適配類對沒有標註 @Adaptive 註解的方法會直接拋出異常,而使用了 @Adaptive 註解的方法內部實際是經過 ExtensionLoader 去找到要使用的具體的擴展實現,再調用這個擴展實現的同名方法。

擴展實現的選擇遵循如下邏輯:

  • 讀取 @Adaptive 註解的 value 屬性,若是 value 沒有值則把當前擴展點接口名轉換爲「點分隔」形式,好比 TroubleMaker 轉換爲 trouble.maker。而後用這個做爲 key 從 URL 上去獲取要使用的具體擴展實現。
  • 若是上一步沒有獲取到,則取擴展點接口上的 @SPI 註解的 value 值做爲 key 再去 URL 上獲取。

怎麼啓用擴展實現

有些擴展點的擴展實現是能夠同時使用多個的,而且能夠按照實際需求來啓用,好比 Filter 擴展點的衆多擴展實現。這就帶來兩個問題,一個是怎麼啓用擴展,另外一個是擴展是否能夠啓用。Dubbo 提供了 @Activate 註解來標註擴展的啓用條件。

public @interface Activate {
  String[] group() default {};
  String[] value() default {};
}
複製代碼

衆所周知 Dubbo 分爲客戶端和服務端兩側,group 用來指定擴展能夠在哪一端啓用,取值只能是 consumerprovider,對應的常量位於 CommonConstants。value 用來指定擴展實現的開啓條件,也就是說若是 URL 上能經過 getParameter(value) 方法獲取一個不爲空(即不爲 false0nullN/A)的值,那麼這個擴展實現就會被啓用。

例如存在一個 Filter 擴展點的擴展實現 FilterX:

@Activate(group = {CommonConstants.PROVIDER}, value = "x")
public class FilterX implements Filter {}
複製代碼

若是當前是服務端一側在加載擴展實現,而且 url.getParameter("x") 能拿到一個不爲空的值,那 FilterX 這個擴展實現就會被啓用。須要注意的是,@Activate 的 value 屬性的值不須要和 SPI 配置文件裏的 key 保持一致,而且 value 能夠是個數組

啓用擴展實現的方式

第一種啓用方式就是上文所講的讓 value 做爲 url 的 key 而且值不爲空,另外一種擴展實現的啓用就要回到 ExtensionLoader 的 getActivateExtension(URL url, String key, String group) 方法。

參數 key 表示一個存在於 url 上的參數,這個參數的值指定了要啓用的擴展實現,多個擴展實現之間用逗號分隔,參數 group 表示當前是服務端一側仍是客戶端一側。這個方法把經過參數 key 獲取到的值拆分後調用了重載方法 getActivateExtension(URL url, String[] values, String group),這個方法就是擴展實現啓用的關鍵點所在。

首先是判斷要開啓的擴展實現名稱列表裏有沒有 -default,這裏的 - 是減號,是「去掉」的意思,default 表示默認開啓的擴展實現,因此 -default 的意思就是要去掉默認開啓的擴展實現。所謂默認開啓的擴展實現,其實就是攜帶了 @Activate 註解可是註解的 value 沒有值的那些擴展實現,好比 ConsumerContextFilter。以此推論,若是擴展實現的名稱前帶了 - 就表示這個擴展實現不開啓

若是沒有 -default 接着就是迭代 cachedActivates 去判斷哪些擴展實現是須要使用的,關鍵方法是 isActive(String[] keys, URL url)。這個方法在源碼裏沒有註釋,理解起來可能有些困難。實際上就是判斷傳入的這些 keys 是否在 url 上存在。

這裏有個騷操做,cachedActivates 保存的是「擴展實現名稱」到「@Aactivate」註解的映射,也就是這個 mapvalue 不是擴展實現的 Class 對象或者實例。由於 cachedClassescachedInstances 已經分別保存了二者,只要有擴展實現的名字就能夠獲取到,沒有必要多保存一份。

回到方法的另一個分支,若是有 -default,那就是隻開啓 url 上指定的擴展實現,同時處理一下攜帶了 - 的名稱。方法最後把全部要開啓的擴展實現放入 activateExtensions 集合返回。

啓用擴展實現的示例

我的認爲 Dubbo SPI 這一塊適合採用視頻的方式進行源碼分析,由於這裏面有不少邏輯是相互牽連的,依靠文字不太容易講的明白。因此這裏用一個示例來展現上文講到的擴展實現啓用邏輯。假設如今存在如下 5 個自定義 Filter:

public class FilterA implements Filter {}

@Activate(group = {CommonConstants.PROVIDER}, order = 2)
public class FilterB implements Filter {}

@Activate(group = {CommonConstants.CONSUMER}, order = 3)
public class FilterC implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 4)
public class FilterD implements Filter {}

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER}, order = 5, value = "e")
public class FilterE implements Filter {}
複製代碼

配置文件 META-INF/dubbo/internal/org.apache.dubbo.rpc.Filter

fa=org.apache.dubbo.rpc.demo.FilterA
fb=org.apache.dubbo.rpc.demo.FilterB
fc=org.apache.dubbo.rpc.demo.FilterC
fd=org.apache.dubbo.rpc.demo.FilterD
fe=org.apache.dubbo.rpc.demo.FilterE
複製代碼

首先直接查找消費者端(Consumer)可使用的 Filter 擴展點的擴展實現:

public static void main(String[] args) {
  ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
  URL url = new URL("", "", 10086);
  List<Filter> activate = extensionLoader.getActivateExtension(url, "", CommonConstants.CONSUMER);
  activate.forEach(a -> System.out.println(a.getClass().getName()));
}
// 輸出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
複製代碼

能夠看到自定義擴展實現裏的 C 和 D 被啓用。A 因爲沒有 @Activate 註解不會默認啓用,B 限制了只能在服務端(Provider)啓用,E 的 @Activate 註解的 value 屬性限制了 URL 上必須存在名叫 e 的參數能夠被啓用。

接下來添加參數嘗試讓 E 被啓用:

URL url = new URL("", "", 10086).addParameter("e", (String) null);
// 輸出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
複製代碼

能夠看到 E 仍是沒被啓用,這是由於雖然 URL 上存在了名爲 e 的參數,可是值爲空,不符合啓用規則,這時候只要把值調整爲任何不爲空(即不爲 false0nullN/A)的值就能夠啓用 E 了。

換另外一種方式啓用 E:

URL url = new URL("", "", 3).addParameter("filterValue", "fe");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
// 輸出
// org.apache.dubbo.rpc.filter.ConsumerContextFilter
// org.apache.dubbo.rpc.demo.FilterC
// org.apache.dubbo.rpc.demo.FilterD
// org.apache.dubbo.rpc.demo.FilterE
複製代碼

添加參數 filterValue 並指定值爲 fe,這裏的值要和 SPI 配置文件裏的 key 保持一致。調用 getActivateExtension() 方法時指定這個參數的名字,這時就能夠看到 E 被啓用了。

接下來試試去掉默認開啓的擴展實現並指定 A 啓用:

URL url = new URL("", "", 3).addParameter("filterValue", "fa,-default");
List<Filter> activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
// 輸出
// org.apache.dubbo.rpc.demo.FilterA
複製代碼

加上 -default 後 ConsumerContextFilter 和 C 、D 被禁用了,由於他們是默認開啓的實現。再回憶一次,默認開啓的擴展實現其實就是攜帶了 @Activate 註解可是註解的 value 沒有值的那些擴展實現。儘管 A 沒有攜帶 @Activate 註解,可是這裏指定了須要啓用,因此 A 被啓用。

最後

好了,終於分析完了 Dubbo 的這一套 SPI 機制,其實也不算太複雜,只是邏輯繞了一點,有機會我會將本文錄製爲視頻講解,但願能讓你們有更好的理解。

相關文章
相關標籤/搜索