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

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

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

怎麼獲取擴展實現

在 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() 方法真的去載入了。

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 欄位用來表示擴展點的默認擴展實現的名稱。

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() 方法要排序的原因。

查找和解析 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 機制,其實也不算太複雜,只是邏輯繞了一點,有機會我會將本文錄製為影片講解,希望能讓大家有更好的理解。

Tags: