Dubbo SPI 機制原始碼分析(基於2.7.7)
阿新 • • 發佈:2021-02-10
Dubbo SPI 機制涉及到 `@SPI`、`@Adaptive`、`@Activate` 三個註解,ExtensionLoader 作為 Dubbo SPI 機制的核心負責載入和管理擴充套件點及其實現。本文以 ExtensionLoader 的原始碼作為分析主線,進而引出三個註解的作用和工作機制。
ExtensionLoader 被設計為只能通過 `getExtensionLoader(Class type)` 方法獲取到例項,引數 `type` 表示拿到的這個例項要負責載入的擴充套件點型別。為了避免在之後的原始碼分析中產生困惑,請先記住這個結論:**每個 ExtensionLoader 只能載入其繫結的擴充套件點型別(即 type 的型別)的具體實現**。也就是說,如果 `type` 的值是 `Protocol.class`,那麼這個 ExtensionLoader 的例項就只能載入 Protocol 介面的實現,不能去載入 Compiler 介面的實現。
# 怎麼獲取擴充套件實現
在 Dubbo 裡,如果一個介面標註了 `@SPI` 註解,那麼它就表示一個擴充套件點型別,這個介面的實現就是這個擴充套件點的實現。比如 Protocol 介面的宣告:
```java
@SPI("dubbo")
public interface Protocol {}
```
一個擴充套件點可能存在多個實現,可以使用 `@SPI` 註解的 `value` 屬性指定要選擇的預設實現。當用戶沒有明確指定要使用哪個實現時,Dubbo 就會自動選擇這個預設實現。
`getExtension(String name)` 方法可以獲取指定名稱的擴充套件實現的例項,這個擴充套件實現的型別必須是當前 ExtensionLoader 繫結的擴充套件型別。這個方法會先查快取裡是否有這個擴充套件實現的例項,如果沒有再通過 `createExtension(String name)` 方法建立例項。Dubbo 在這一塊設定了多層快取,進入 `createExtension(String name)` 方法後又會呼叫 `getExtensionClasses()` 方法拿到當前 ExtensionLoader 已載入的所有擴充套件實現。如果還拿不到,那就呼叫 `loadExtensionClasses()` 方法真的去載入了。
```java
private Map> loadExtensionClasses() {
// 取 @SPI 註解上的值(只允許存在一個值)儲存到 cachedDefaultName
cacheDefaultExtensionName();
Map> extensionClasses = new HashMap<>();
// 不同的策略代表不同的目錄,迭代進行載入
for (LoadingStrategy strategy : strategies) {
// loadDirectory(...)
// 執行不同策略
}
return extensionClasses;
}
```
`cacheDefaultExtensionName()` 方法會從當前 ExtensionLoader 繫結的 `type` 上去獲取 `@SPI` 註解,並將其 `value` 值儲存到 ExtensionLoader 的 `cachedDefaultName` 欄位用來表示擴充套件點的預設擴充套件實現的名稱。
## SPI 配置的載入策略
接著迭代三種擴充套件實現載入策略。`strategies` 是通過 `loadLoadingStrategies()` 方法載入的,在這個方法裡已經對三種策略進行了優先順序排序,排序規則是**低優先順序的策略放在前面**。簡單看一下 LoadingStrategy 介面:
```java
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()` 方法處理。
```java
private void loadClass(Map> 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:
```java
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 Exporter export(Invoker invoker) throws RpcException {
if (UrlUtils.isRegistry(invoker.getUrl())) { // 一些額外的邏輯
startQosServer(invoker.getUrl());
return protocol.export(invoker);
}
return protocol.export(invoker);
}
@Override
public Invoker refer(Class 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` 形式,那麼就把擴充套件實現類的類名作為 `key`。`cacheActivateClass()` 方法用於判斷當前擴充套件實現是否攜帶了 `@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 type, String name)` 方法。
```java
public T getExtension(Class 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 介面:
```java
@SPI("dubbo")
public interface Protocol {
@Adaptive
Exporter export(Invoker invoker) throws RpcException;
@Adaptive
Invoker refer(Class 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()` 方法動態生成一個適配類。這裡涉及到的幾個方法很簡單就不貼程式碼了,下面看一下動態生成適配的方法。
```java
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()` 方法實際就是做的適配類檔案的字串拼接。具體的生成邏輯沒有什麼好講的,都是些字串操作,這裡簡單寫個示例:
```java
@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` 註解來標註擴充套件的啟用條件。
```java
public @interface Activate {
String[] group() default {};
String[] value() default {};
}
```
眾所周知 Dubbo 分為客戶端和服務端兩側,`group` 用來指定擴充套件可以在哪一端啟用,取值只能是 `consumer` 或 `provider`,對應的常量位於 CommonConstants。`value` 用來指定擴充套件實現的開啟條件,也就是說如果 URL 上能通過 `getParameter(value)` 方法獲取一個不為空(即不為 `false`、`0`、`null` 和 `N/A`)的值,那麼這個擴充套件實現就會被啟用。
例如存在一個 Filter 擴充套件點的擴充套件實現 FilterX:
```java
@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`」註解的對映,也就是這個 `map` 的 `value` 不是擴充套件實現的 Class 物件或者例項。因為 `cachedClasses` 和 `cachedInstances` 已經分別儲存了兩者,只要有擴充套件實現的名字就可以獲取到,沒有必要多儲存一份。
回到方法的另外一個分支,如果有 `-default`,那就是隻開啟 `url` 上指定的擴充套件實現,同時處理一下攜帶了 `-` 的名稱。方法最後把所有要開啟的擴充套件實現放入 `activateExtensions` 集合返回。
## 啟用擴充套件實現的示例
個人認為 Dubbo SPI 這一塊適合採用視訊的方式進行原始碼分析,因為這裡面有很多邏輯是相互牽連的,依靠文字不太容易講的明白。所以這裡用一個示例來展示上文講到的擴充套件實現啟用邏輯。假設現在存在以下 5 個自定義 Filter:
```java
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`
```properties
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 擴充套件點的擴充套件實現:
```java
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
URL url = new URL("", "", 10086);
List 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 被啟用:
```java
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` 的引數,但是值為空,不符合啟用規則,這時候只要把值調整為任何不為空(即不為 `false`、`0`、`null` 和 `N/A`)的值就可以啟用 E 了。
換另一種方式啟用 E:
```java
URL url = new URL("", "", 3).addParameter("filterValue", "fe");
List 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 啟用:
```java
URL url = new URL("", "", 3).addParameter("filterValue", "fa,-default");
List activate = extensionLoader.getActivateExtension(url, "filterValue", CommonConstants.CONSUMER);
// 輸出
// org.apache.dubbo.rpc.demo.FilterA
```
加上 `-default` 後 ConsumerContextFilter 和 C 、D 被禁用了,因為他們是預設開啟的實現。再回憶一次,預設開啟的擴充套件實現其實就是攜帶了 `@Activate` 註解但是註解的 `value` 沒有值的那些擴充套件實現。儘管 A 沒有攜帶 `@Activate` 註解,但是這裡指定了需要啟用,所以 A 被啟用。
# 最後
好了,終於分析完了 Dubbo 的這一套 SPI 機制,其實也不算太複雜,只是邏輯繞了一點,有機會我會將本文錄製為視訊講解,希望能讓大家有更好的