4.Dubbo的SPI擴展點加載機制

4.1 加載機制概述

4.1.1 Java SPI

在講Dubbo SPI之前,先來了解一下Java SPI,SPI全稱叫Service Provider Interface,起初是提供給廠商做插件開發(fā)的。Java SPI使用了策略模式,一個接口多種實現(xiàn),我們只聲明接口,具體實現(xiàn)由程序之外的配置掌控,步驟如下:

  • 定義一個接口及對應的方法。
  • 編寫該接口的一個實現(xiàn)類。
  • 在META-INF/services目錄下,創(chuàng)建一個以接口全路徑命名的文件,如com.test.spi.PrintService。
  • 文件內(nèi)容為具體實現(xiàn)類的全路徑名,如果有多個,用分行符隔開。
  • 在代碼通過java.util.ServiceLoader來加載具體的實現(xiàn)類。
public static void main(String[] args) {
  ServiceLoader<PrintService> serviceLoader = ServiceLoader.load(PrintService.class);
  for (PrintService printService : serviceLoader) {
    printService.printInfo();
  }
}

4.1.2 Dubbo對SPI的改進

  • Java SPI會一次性實例化擴展點所有實現(xiàn),如果有擴展實現(xiàn)則初始化很耗時,如果沒用上,則造成資源浪費。
  • Java SPI如果擴展失敗,如ScriptEngine的實現(xiàn)類RubyScriptEngine因為所依賴的jruby.jar不存在,導致加載失敗,這個失敗原因會被“吃掉”,當用戶執(zhí)行Ruby腳本,會報不支持Ruby,而不是真正的原因。
  • Dubbo增加了對擴展IOC和AOP的支持。
// 第一步,在目錄/META-INF/dubbo/internal下簡歷配置文件com.test.spi.PrintService
// 文件內(nèi)容:impl=com.test.spi.PrintServiceImpl

// 第二步,接口加上SPI注解
@SPI("impl")
public interface PrintService {
  void printInfo();
}

public class PrintServiceImpl implements PrintService {
  @Override
  public void printInfo() {
    System.out.println("hello");
  }
}

// 第三步,通過ExtensionLoader加載
public static void main(String[] args) {
  PrintService printService = ExtensionLoader.getExtensionLoader(PrintService.class).getDefaultExtension():
  pritnService.printInfo();
}

4.1.3 Dubbo擴展點的配置規(guī)范

Dubbo SPI和Java SPI類似,需要在META-INF/dubbo/下放置對應的SPI配置文件,文件名為接口的全路徑名。配置文件內(nèi)容為key=擴展點實現(xiàn)類全路徑名,如果有多個實現(xiàn)類則使用換行符分隔。其中,key會作為@SPI注解中的傳入?yún)?shù)。另外,Dubbo SPI還同時兼容Java SPI的配置路徑和內(nèi)容配置方式。在Dubbo啟動時,會默認掃四個目錄下的配置:META-INF/services、META-INF/dubbo、META-INF/dubbo/internal 、META-INF/dubbo/external。

4.1.4 Dubbo擴展點的分類與緩存

Dubbo SPI可以分為Class緩存、實例緩存。

  • Class緩存:Dubbo SPI獲取擴展類時,會先從緩存讀取,如果緩存中不存在,則加載配置文件,根據(jù)配置把Class緩存到內(nèi)存中,但并不會直接實例化。
  • 實例緩存:每次獲取實例時,同樣是先從實例緩存中讀取,如果讀不到,則重新加載并緩存。因此Dubbo SPI是按需實例化并緩存的機制,性能更好。

被緩存的Class和對象實例可以根據(jù)不同的特性分為不同的類別:

  • 普通擴展類。最基礎的,配置在SPI配置文件中的擴展類實現(xiàn)。
  • 包裝擴展類。這種Wrapper類沒有具體實現(xiàn),只是做了通用邏輯的抽象,并且需要在構造方法中傳入一個 具體的擴展接口的實現(xiàn)。屬于Dubb的自動包裝特性。
  • 自適應擴展類。一個擴展接口會有多種實現(xiàn)類,具體使用哪個實現(xiàn)類可以不寫死在配置或代碼中,在運行時,通過傳入的URL中的某些參數(shù)動態(tài)選擇實現(xiàn)類,會使用@Adaptive注解。
  • 其他緩存,如擴展類加載器緩存、擴展名緩存等。

4.1.5 擴展點的特性

  1. 自動包裝(AOP)
    ExtensionLoader在加載擴展時,如果發(fā)現(xiàn)這個擴展類的構造方法有其他擴展點作為構造函數(shù),則會被認為是一個Wrapper類。
public class ProtocolFilterWrapper implements Protocol {
  private final Protocol protocol;
  public ProtocolFilterWrapper(Protocol protocol) {
    if (protocol == null) {
      throw new IllegalArgumentException("protocol == null");
    }
    this.protocol = protocol;
  }
  ...
}
  1. 自動加載(IOC)
    如果某個擴展類是另外一個擴展點類的成員屬性,并且擁有setter方法,那么框架會自動注入對應的擴展點實例。此處存在一個問題,如果依賴的擴展有多種實現(xiàn),具體會注入哪一個呢?這就涉及到第3個特性-自適應了。
    3.自適應
    在Dubbo SPI中,我們可以使用@Adaptive注解,動態(tài)地依據(jù)URL中的某個參數(shù)來確定使用哪個實現(xiàn)類,從而解決自動加載中的實例注入問題。
@SPI("netty")
public interface Transproter {
  @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
  Server bind(URL url, ChannelHandler handler) throws RemotingException;

  @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
  Client connect(URL url, ChannelHandler handler) throws RemotingException;
}

@Adaptive傳入了兩個參數(shù),分別是“server”和"transporter",外部調(diào)用Transporter#bind方法時,會動態(tài)從傳入的參數(shù)URL中提取key參數(shù)“server”對應的value值,如果能匹配上唯一一個擴展實現(xiàn)類則直接使用對應的實現(xiàn)類;如果不能匹配,則繼續(xù)通過第二個key參數(shù)"transporter"的value值來匹配。都沒有匹配上,則拋出異常。
4.自動激活
使用@Activate注解,可以標記對應的擴展點默認被激活啟用。該注解可以通過傳入不同的參數(shù),設置擴展點在不同的條件下被自動激活。這個注解的主要使用場景是某個擴展點的多個實現(xiàn)類需要同時啟用(比如Filter擴展點)。

4.2 擴展點注解

4.2.1 @SPI

@SPI注解通常用在接口上,標記這個接口是一個Dubbo SPI接口。該注解可以接收一個value參數(shù),用于指定這個接口的默認實現(xiàn)類。

4.2.2 @Adaptive

@Adaptive可以標記在類、接口、方法上,但大多數(shù)情況用在方法上更多。方法級別注解在第一次getExtension時,會自動生成和編譯一個動態(tài)的Adaptive類,從而打到動態(tài)實現(xiàn)類的效果。
如前面的例子,Transproter接口在bind和connect方法上添加了@Adaptive注解,Dubbo在初始化擴展點時,會生成一個Transporter$Adaptive類,里面會實現(xiàn)這兩個方法,方法里會有一些抽象的通用邏輯,通過@Adaptive中傳入的參數(shù)找到并調(diào)用真正的實現(xiàn)類,跟裝飾者模式有點類似。下面是自動生成的Transporter$Adaptive#bind的代碼:

public Server bind(URL arg0, ChannelHandler arg1)  throws RemotingException {
  URL url = arg0;
  String extName = url.getParameter("server", url.getParameter("transporter", "netty"));
  ...
  try {
    extension = (Transporter)ExtensionLoader.getExtensionLoader(Transporter.class).getExtension(extName);
  } catch (Exception e) {
    extension = (Transporter)ExtensionLoader.getExtensionLoader(Transporter.class).getExtension("netty");
  }
  return extension.bind(arg0, arg1);
}

如果該注解放在實現(xiàn)類上,則整個實現(xiàn)類會直接作為默認實現(xiàn),不需要自動生成上面的動態(tài)代碼,如果有多個實現(xiàn)類,只有一個實現(xiàn)類可以加上@Adaptive注解,萬一多個實現(xiàn)類都注解了會拋異常。因此在實現(xiàn)類上使用這個注解的效果,與@SPI的參數(shù)指定實現(xiàn)類類似(但讀完后面的源代碼其實還是不一樣的,@SPI指定的默認實現(xiàn)類用于getExtension方法,@Adaptive指定的用于getAdaptiveExtension方法)。

4.2.3 @Activate

@Activate可以標記在類、接口和方法上。主要用于有多個擴展點實現(xiàn),需要根據(jù)不同條件來激活的場景,比如Filter需要多個同時激活。@Activate可傳入的參數(shù)很多:

  • String[] group 指定URL中的分組,如果匹配則激活,可以設置多個。
  • String[] value 查找URL如果含有該key的值,則會激活。
  • String[] before 填寫擴展點列表,表示這些擴展點要在本擴展點之前
  • String[] after 同上,表示之后
  • int order 直接的順序信息

4.3 ExtensionLoader的工作原理

4.3.1 工作流程

ExtensionLoader的邏輯入口可以分為getExtension、getAdaptiveExtension、getActivateExtension三個,總體邏輯都是從調(diào)用這三個方法開始的。

  1. getActivateExtension只是根據(jù)不同的條件同時激活多個普通擴展類,因此此方法只會做一些通用的判斷邏輯,比如是否包含@Activate注解,匹配條件是否符合等。最終還是會調(diào)用getExtension方法來獲得具體的實現(xiàn)類實例。

  2. getExtension(String name)是整個擴展加載器最核心的方法,實現(xiàn)了一個完整的普通擴展類加載的過程。加載過程中的每一步,都會先檢查緩存中是否已經(jīng)存在所需的數(shù)據(jù),如果存在則直接從緩存中讀取,沒有則重新加載。這個方法每次只會根據(jù)名稱返回一個擴展點實現(xiàn)類。實例化的過程可以分為4步:
    (1) 讀取SPI對應路徑下的配置文件,并根據(jù)配置加載所有擴展類Class并緩存。(這個應該是只讀getExtensionLoader(Class)傳遞的class對象所對應的配置文件)
    (2) 根據(jù)傳入的name參數(shù)實例化對應的擴展類。
    (3) 嘗試查找符合條件的包裝類:包含擴展點的setter方法;包含與擴展類型相同的構造函數(shù),例如本次實例化了一個Class A對象。實例化完成后,會尋找構造參數(shù)中需要Class A的包裝類。(居然是主動查找?)
    (4) 返回對應的擴展類實例。

  3. getAdaptiveExtension也相對獨立,只加載配置信息部分與getExtension共用了同一個方法。和獲取普通擴展類一樣,框架會先檢查緩存中是否有已經(jīng)實例化好的Adaptive實例,沒有則調(diào)用createAdaptiveExtension進行實例化。
    (1) 和getExtension()一樣先加載配置文件。
    (2) 生成自適應類的代碼字符串。
    (3) 獲取類加載器和編譯器,并用編譯器編譯剛才生成的代碼字符串。Dubbo一共有三種類型的編譯器實現(xiàn),后面會講。
    (4)返回對應的自適應類實例。

4.3.2 getExtension的實現(xiàn)原理

用法示例

ExtensionLoader.getExtensionLoader(XXX.class).getExtension("xxx"):

當調(diào)用getExtension(String name)方法時,會先檢查緩存中是否有現(xiàn)成的數(shù)據(jù),沒有則調(diào)用createExtension開始創(chuàng)建。這里有個特殊點,如果getExtension的name參數(shù)時"true",會加載并返回默認擴展類,內(nèi)部實質(zhì)會調(diào)用getDefaultExtension()。
在調(diào)用createExtension的過程中,也會先檢查緩存中是否有配置信息,如果不存在,則會從META-INF/services、META-INF/dubbo、META-INF/dubbo/internal、META-INF/dubbo/external這幾個路徑中讀取所有的配置文件,得到擴展點實現(xiàn)類的全稱。


getExtension

createExtension

先看createExtension()中通過getExtensionClasses()獲取實現(xiàn)類Class的實現(xiàn):


ExtensionLoader#getExtensionClasses

ExtensionLoader#loadExtensionClasses

ExtensionLoader#loadDirectory

ExtensionLoader#loadResource

ExtensionLoader#loadClass

接下來再回頭看createExtension()拿到實現(xiàn)類Class之后,剩余的部分:


ExtensionLoader#createExtension

ExtensionLoader#injectExtension

個人思考與嘗試:
1.有一點不太理解為什么依賴注入要用getAdaptiveExtension而不用getExtension呢?我自己寫了兩個@SPI接口,實現(xiàn)類和接口都沒有@Adaptive注解測試了下依賴注入,發(fā)現(xiàn)getAdaptiveExtension確實會拋異常,必須要求依賴注入的@SPI接口是自適應類型的,這個設計有點奇怪。不過通過查看dubbo本身的源碼,用到依賴注入的確實基本上都是@Adaptive的。
2.關于循環(huán)依賴,我自己寫了兩個@Adaptive類互相依賴,發(fā)現(xiàn)并不會出現(xiàn)異常,看了很久發(fā)現(xiàn)原來是因為依賴注入的必然是XXX$Adaptive類,此類是通過代碼生成再編譯的方式得到的,通過調(diào)試和反編譯看到生成的類內(nèi)部不會再有更深層依賴注入,而是在用到依賴的地方使用ExtensionLoader.getExtensionLoader().getExtension()的方式來延遲獲取。其實想明白Adaptive本身就是要動態(tài)切換所依賴的實現(xiàn)類這層含義,就不會有循環(huán)依賴這個疑問。

4.3.3 getAdaptiveExtension的實現(xiàn)原理

由前面的流程我們可以知道,在getAdaptiveExtension()方法中,會為擴展點接口自動生成實現(xiàn)類字符串,實現(xiàn)類主要包含以下邏輯:為接口中每個有@Adaptive注解的方法生成默認實現(xiàn)(沒有注解的方法則生成空實現(xiàn)),每個默認實現(xiàn)都會從URL(或者directory中的url)中提取Adaptive參數(shù)值,并以此為依據(jù)動態(tài)加載擴展點。然后,框架會使用不同的編譯器,把實現(xiàn)類代碼字符串編譯成Class并返回。
生成代碼的邏輯主要分7步:
(1) 生成package、import、類名稱等頭部信息。此處import只有ExtensionLoader一個類。為了步import其他類,其他類都用全路徑來使用。生成的類名稱為“接口名稱$Adaptive”。
(2) 遍歷接口所有方法,獲取方法的返回類型、參數(shù)類型、異常類型。為第(3)步判斷是否為空做準備。
(3) 生成校驗代碼,如參數(shù)是否為空。如果有遠程調(diào)用,還會添加Invocation參數(shù)是否為空的校驗。
(4) 如果@Adaptive注解沒有設定value,生成默認url參數(shù)名,按照類名稱駝峰轉點的方式生成,如YyyInvokerWrapper會以yyy.invoker.wrapper作為默認url參數(shù)名。
(5) 生成獲取擴展點名稱的代碼。如@Adaptive("protocol"),會生成url.getProtocol()。
(6) 生成獲取具體擴展實現(xiàn)類的代碼。即通過ExtensionLoader.getExtensionLoader().getExtension(extName)獲取到url中自適應參數(shù)對于的實現(xiàn)類。如果url中拿不到參數(shù)值,則換下一個參數(shù)值,如果都不行,則使用默認實現(xiàn)類別名來作為extName去加載實現(xiàn)類實例。
(7) 生成調(diào)用實現(xiàn)類方法的代碼。

書中居然省略了這么重要的自適應的源碼分析。。。無語,具體可參考官網(wǎng):
https://dubbo.apache.org/zh/docs/v2.7/dev/source/adaptive-extension/

4.3.4 getActivateExtension的實現(xiàn)原理

通過getActivateExtension(URL url, String key, String group)方法可以獲取所有自動激活的擴展點。其流程分4步:
(1) 檢查緩存,如果緩存中沒有,則初始化所有擴展類實現(xiàn)的集合。
(2) 遍歷整個@Activate注解集合,根據(jù)傳入的URL、key、group參數(shù)匹配符合條件的擴展類實現(xiàn)。然后根據(jù)@Activate中配置的before、after、order等參數(shù)進行排序。
(3) 遍歷所有用戶自定義擴展類名稱,根據(jù)用戶URL配置的順序,調(diào)整擴展點激活順序(遵循用戶在URL中配置的順序,例如URL為test://localhost/test?ext=order1,default,那么擴展點ext的激活順序會遵循先order1再default,其中default代表所有@Activate注解的擴展點)
(4) 返回所有自動激活類集合。
注意點1:獲取Activate擴展類實現(xiàn),也是通過調(diào)用getExtension得到的。
注意點2:如果URL的參數(shù)中傳入了-default,則所有默認的@Activate都不會被激活,只有URL參數(shù)中指定的擴展點會被激活。如果傳入了"-"+擴展點名,則該擴展點也不會被自動激活。

ExtensionLoader#getActivateExtension

雖然這段代碼實現(xiàn)相對不那么重要,但因為不容易看明白,所以還是貼以下代碼并加了點方便理解的注釋。

4.3.5 ExtensionFactory的實現(xiàn)原理

前面在依賴注入的實現(xiàn)中已經(jīng)有提到過,通過ExtensionLoader中的objectFactory除了可以注入其他擴展點,還能注入Spring bean,這個又是如何實現(xiàn)的呢?我們知道objectFacotry的類型是ExtensionFactory,這是一個@SPI接口提供了SpiExtensionFactory、SpringExtensionFactory、AdaptiveExtensionFactory三個實現(xiàn)。


ExtensionFactory

那么具體用的是哪個實現(xiàn)呢?翻看源碼可以知道AdaptiveExtensionFactory在類上使用了@Adaptive注解,所以在ExtensionLoader的構造方法中調(diào)用getAdaptiveExtension()會返回AdaptiveExtensionFactory實例。


ExtensionLoader構造方法

AdaptiveExtensionFactory

再來看SpiExtensionFactory的實現(xiàn):
SpiExtensionFactory

最后是SpringExtensionFactory:


SpringExtensionFactory#getExtension

那么Spring的上下文又是什么時候被保存起來的呢?我們通過代碼搜索得知,在ReferenceBean和ServiceBean中會調(diào)用靜態(tài)方法保存Spring上下文,即一個服務被發(fā)布或者被引用的時候。

4.4 擴展點動態(tài)編譯的實現(xiàn)

動態(tài)編譯是自適應特性的基礎,因為動態(tài)生成的自適應類只是字符串,需要通過編譯才能得到真正的Class。雖然我們可以通過反射來動態(tài)代理一個類,但是在性能上和直接編譯好的Class會有一定的差距,所以Dubbo SPI采用代碼動態(tài)生成,并配合動態(tài)編譯器,靈活地在原始類的基礎上創(chuàng)建新的自適應類。
Dubbo有三種代碼編譯器,分別是JdkCompiler,JavassistCompiler和AdaptiveCompiler,他們都實現(xiàn)了Complier接口。接口Compiler上含有@SPI注解,默認值為@SPI("javassist"),即使用JavassistCompiler作為默認編譯器。用戶想修改可以通過配置<dubbo:application compiler="jdk" />進行修改。
AdaptiveCompiler類上面有@Adaptive注解,說明它作為自適應的默認實現(xiàn),其作用與AdaptiveExtensionFactory類似,用于管理其他Compiler。


AdaptiveCompiler

然后看一下AbstractCompiler,它是一個抽象類,里面封裝了通用的模板邏輯,還定義了一個抽象方法doCompile,留給子類來實現(xiàn)。
AbstractCompiler的主要抽象邏輯:

  • 通過正則匹配出包路徑、類名,在根據(jù)包路徑、類名拼接出全路徑類名。
  • 嘗試通過Class.forName加載該類,防止重復編譯。如果類加載器中沒有,則進入下一步。
  • 調(diào)用doCompile方法進行編譯。

4.4.2 Javassist動態(tài)代碼編譯

先看一個Javassist生成Hello World的使用例子:

ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("HelloWord");
CtMethod ctMethod = CtNewMethod.make("
    public static void test() {
        System.out.println(\"Hello Word\");
    }
", ctClass);
ctClass.addMethod(ctMethod);
Class clazz = ctClass.toClass();
Object object = clazz.newInstance();
Method m = clazz.getDeclaredMethod("test", null);
m.invoke(object, null);

看完使用示例,Dubbo的JavassistCompiler的實現(xiàn)就很容易猜到了,就是通過正則匹配不同部位的代碼,然后調(diào)用Javassist的api來生成添加進ctClass中,最后得到一個完整的Class對象。具體步驟:
(1) 初始化Javassist,如設置classpath。
(2) 正則匹配所有import包,使用Javassist添加import。
(3) 正則匹配所有extends,創(chuàng)建Class對象,并使用Javassist添加extends。
(4) 正則匹配所有implements,并使用Javassist添加implements。
(5) 正則匹配類里面{}中所有的內(nèi)容,在通過正則匹配所有的方法,并使用Javassist添加類方法。
(6) 生成Class對象。

4.4.3 JDK動態(tài)編譯

JDK自帶的編譯器位于javax.tools下,Dubbo主要使用了JavaFileObject接口、JavaFileManager接口、JavaCompiler.CompilationTask方法。整個編譯過程可以總結為:先初始化一個JavaFileObject對象,并把代碼字符串作為參數(shù)傳入構造方法,然后調(diào)用JavaCompiler.CompilationTask方法編譯出具體的類。JavaFileManager負責管理類文件的輸入/輸出位置。

  • JavaFileObject接口。字符串代碼會被包裝成一個文件對象,并提供二進制流的接口。Dubbo框架中的JavaFileObjectImpl類可以看作該接口一種擴展實現(xiàn),構造方法中需要傳入生成好的字符串代碼,此文件對象的輸入和輸出都是ByteArray流。
  • JavaFileManager接口。主要管理文件的讀取和輸出位置。JDK中沒有可以直接使用的實現(xiàn)類,唯一的實現(xiàn)類是ForwardingJavaFileManager構造器又是protect類型。因此Dubbo中定制化實現(xiàn)了一個JavaFileManagerImpl類,并通過一個自定義加載器ClassLoaderImpl完成資源的加載。
  • JavaCompiler.CompilationTask把JavaFileObject對象編譯成具體的類。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容