SPI 全稱為 Service Provider Interface,是一種服務發現機制。
SPI 的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類。這樣可以在運作時,動态為接口替換實作類。正是以特性,我們可以很容易的通過 SPI 機制為我們的程式提供拓展功能。
SPI 機制在第三方架構中也有所應用,比如 Dubbo 就是通過 SPI 機制加載所有的元件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的子產品。基于 SPI,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學習 Dubbo 的源碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然後再來分析 Dubbo SPI 的源碼。
需要特别說明的是,本篇文章以及本系列其他文章所分析的源碼版本均為 dubbo-2.6.4。是以大家在閱讀文章的過程中,需注意将代碼版本切換到 dubbo-2.6.4 tag 上。
前面簡單介紹了 SPI 機制的原理,本節通過一個示例示範 Java SPI 的使用方法。首先,我們定義一個接口,名稱為 Robot。
接下來定義兩個實作類,分别為 OptimusPrime 和 Bumblebee。
接下來 META-INF/services 檔案夾下建立一個檔案,名稱為 Robot 的全限定名 org.apache.spi.Robot。檔案内容為實作類的全限定的類名,如下:
做好所需的準備工作,接下來編寫代碼進行測試。
最後來看一下測試結果,如下:

從測試結果可以看出,我們的兩個實作類被成功的加載,并輸出了相應的内容。關于 Java SPI 的示範先到這裡,接下來示範 Dubbo SPI。
Dubbo 并未使用 Java SPI,而是重新實作了一套功能更強的 SPI 機制。
Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實作類。
Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,
配置内容如下。
與 Java SPI 實作類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需加載指定的實作類。另外,在測試 Dubbo SPI 時,需要在 Robot 接口上标注 @SPI 注解。下面來示範 Dubbo SPI 的用法:
測試結果如下
Dubbo SPI 除了支援按需加載接口實作類,還增加了 IOC 和 AOP 等特性,這些特性将會在接下來的源碼分析章節中一一進行介紹。
上一章簡單示範了 Dubbo SPI 的使用方法。我們首先通過 ExtensionLoader 的 getExtensionLoader 方法擷取一個 ExtensionLoader 執行個體,然後再通過 ExtensionLoader 的 getExtension 方法擷取拓展類對象。這其中,getExtensionLoader 方法用于從緩存中擷取與拓展類對應的 ExtensionLoader,若緩存未命中,則建立一個新的執行個體。該方法的邏輯比較簡單,本章就不進行分析了。下面我們從 ExtensionLoader 的 getExtension 方法作為入口,對拓展類對象的擷取過程進行詳細的分析。
上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則建立拓展對象。下面我們來看一下建立拓展對象的過程是怎樣的。
createExtension 方法的邏輯稍複雜一下,包含了如下的步驟:
通過 getExtensionClasses 擷取所有的拓展類
通過反射建立拓展對象
向拓展對象中注入依賴
将拓展對象包裹在相應的 Wrapper 對象中
以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實作。在接下來的章節中,将會重點分析 getExtensionClasses 方法的邏輯,以及簡單介紹 Dubbo IOC 的具體實作。
我們在通過名稱擷取拓展類之前,首先需要根據配置檔案解析出拓展項名稱到拓展類的映射關系表(Map<名稱, 拓展類>),之後再根據拓展項名稱從映射關系表中取出相應的拓展類即可。相關過程的代碼分析如下:
這裡也是先檢查緩存,若緩存未命中,則通過 synchronized 加鎖。加鎖後再次檢查緩存,并判空。此時如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯。
loadExtensionClasses 方法總共做了兩件事情,
一是對 SPI 注解進行解析,
二是調用 loadDirectory 方法加載指定檔案夾配置檔案。
SPI 注解解析過程比較簡單,無需多說。
下面我們來看一下 loadDirectory 做了哪些事情。
loadDirectory 方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作。
loadResource 方法用于讀取和解析配置檔案,并通過反射加載類,最後調用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
如上,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,該方法沒有其他什麼邏輯了。
到此,關于緩存類加載的過程就分析完了。整個過程沒什麼特别複雜的地方,大家按部就班的分析即可,不懂的地方可以調試一下。接下來,我們來聊聊 Dubbo IOC 方面的内容。
Dubbo IOC 是通過 setter 方法注入依賴。Dubbo 首先會通過反射擷取到執行個體的所有方法,然後再周遊方法清單,檢測方法名是否具有 setter 方法特征。若有,則通過 ObjectFactory 擷取依賴對象,最後通過反射調用 setter 方法将依賴設定到目标對象中。整個過程對應的代碼如下:
在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部維護了一個 ExtensionFactory 清單,用于存儲其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于建立自适應的拓展,後者是用于從 Spring 的 IOC 容器中擷取所需的拓展。這兩個類的類的代碼不是很複雜,這裡就不一一分析了。
Dubbo IOC 目前僅支援 setter 方式注入,總的來說,邏輯比較簡單易懂。