SPI是一種簡稱,全名叫 Service Provider Interface,Java本身提供了一套SPI機制,SPI 的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類,這樣可以在運作時,動态為接口替換實作類,這也是很多架構元件實作擴充功能的一種手段。
而今天要說的Dubbo SPI機制和Java SPI還是有一點差別的,Dubbo 并未使用 Java 原生的 SPI 機制,而是對他進行了改進增強,進而可以很容易地對Dubbo進行功能上的擴充。
學東西得帶着問題去學,我們先提幾個問題,再接着看
1.什麼是SPI(開頭已經解釋了)
2.Dubbo SPI和Java原生的有什麼差別
3.兩種實作應該如何寫出來
先定義一個接口:
然後建立兩個類,都實作這個Car接口
然後在項目META-INF/services檔案夾下建立一個名稱為接口的全限定名,com.example.demo.spi.Car。
檔案内容寫上實作類的全限定名,如下:
最後寫一個測試代碼:
執行完的輸出結果:
Dubbo 使用的SPI并不是Java原生的,而是重新實作了一套,其主要邏輯都在ExtensionLoader類中,邏輯也不難,後面會稍帶講一下
看看使用,和Java的差不了太多,基于前面的例子來看下,接口類需要加上@SPI注解:
實作類不需要改動
配置檔案需要放在META-INF/dubbo下面,配置寫法有些差別,直接看代碼:
最後就是測試類了,先看代碼:
執行結果:
@SPI 标記為擴充接口
@Adaptive自适應拓展實作類标志
@Activate 自動激活條件的标記
總結一下兩者差別:
使用上的差別Dubbo使用 ExtensionLoader而不是 ServiceLoader了,其主要邏輯都封裝在這個類中
配置檔案存放目錄不一樣,Java的在 META-INF/services,Dubbo在 META-INF/dubbo, META-INF/dubbo/internal
Java SPI 會一次性執行個體化擴充點所有實作,如果有擴充實作初始化很耗時,并且又用不上,會造成大量資源被浪費
Dubbo SPI 增加了對擴充點 IOC 和 AOP 的支援,一個擴充點可以直接 setter 注入其它擴充點
Java SPI加載過程失敗,擴充點的名稱是拿不到的。比如:JDK 标準的 ScriptEngine,getName() 擷取腳本類型的名稱,如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導緻 RubyScriptEngine 類加載失敗,這個失敗原因是不會有任何提示的,當使用者執行 ruby 腳本時,會報不支援 ruby,而不是真正失敗的原因
前面的3個問題是不是已經能回答出來了?是不是非常簡單
Dubbo SPI使用上是通過ExtensionLoader的getExtensionLoader方法擷取一個 ExtensionLoader 執行個體,然後再通過 ExtensionLoader 的 getExtension 方法擷取拓展類對象。這其中,getExtensionLoader 方法用于從緩存中擷取與拓展類對應的 ExtensionLoader,如果沒有緩存,則建立一個新的執行個體,直接上代碼:
上面這一段代碼主要做的事情就是先檢查緩存,緩存不存在建立擴充對象
接下來我們看看建立的過程:
這段代碼看着繁瑣,其實也不難,一共隻做了4件事情:
1.通過getExtensionClasses擷取所有配置擴充類
2.反射建立對象
3.給擴充類注入依賴
4.将擴充類對象包裹在對應的Wrapper對象裡面
我們在通過名稱擷取擴充類之前,首先需要根據配置檔案解析出擴充類名稱到擴充類的映射關系表,之後再根據擴充項名稱從映射關系表中取出相應的拓展類即可。相關過程的代碼如下:
這裡也是先檢查緩存,若緩存沒有,則通過一次雙重鎖檢查緩存,判空。此時如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類。下面是 loadExtensionClasses 方法的代碼
loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 注解進行解析,二是調用 loadDirectory 方法加載指定檔案夾配置檔案。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情
loadDirectory 方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作
loadResource 方法用于讀取和解析配置檔案,并通過反射加載類,最後調用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
綜上,loadClass方法操作了不同的緩存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等
到這裡基本上關于緩存類加載的過程就分析完了,其他邏輯不難,認真地讀下來加上Debug一下都能看懂的。
從設計思想上來看的話,SPI是對迪米特法則和開閉原則的一種實作。
開閉原則:對修改關閉對擴充開放。這個原則在衆多開源架構中都非常常見,Spring的IOC容器也是大量使用。
迪米特法則:也叫最小知識原則,可以解釋為,不該直接依賴關系的類之間,不要依賴;有依賴關系的類之間,盡量隻依賴必要的接口。
那Dubbo的SPI為什麼不直接使用Spring的呢,這一點從衆多開源架構中也許都能窺探一點端倪出來,因為本身作為開源架構是要融入其他架構或者一起運作的,不能作為依賴被依賴對象存在。
再者對于Dubbo來說,直接用Spring IOC AOP的話有一些架構臃腫,完全沒必要,是以自己實作一套輕量級反而是最優解