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 擴展點的特性
- 自動包裝(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;
}
...
}
- 自動加載(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)用這三個方法開始的。
getActivateExtension只是根據(jù)不同的條件同時激活多個普通擴展類,因此此方法只會做一些通用的判斷邏輯,比如是否包含@Activate注解,匹配條件是否符合等。最終還是會調(diào)用getExtension方法來獲得具體的實現(xiàn)類實例。
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) 返回對應的擴展類實例。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)類的全稱。


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





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


個人思考與嘗試:
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ù)中指定的擴展點會被激活。如果傳入了"-"+擴展點名,則該擴展點也不會被自動激活。

雖然這段代碼實現(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)。

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


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

最后是SpringExtensionFactory:

那么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。

然后看一下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對象編譯成具體的類。