javascript
JDK、Spring、Dubbo SPI 原理介绍
導(dǎo)讀:?
需求變化是程序員生命中唯一不變的事情,本文將介紹 JDK/Spring/Dubbo 中的 SPI 機制,以此來幫助我們編寫出一套可擴展性強,易于維護的代碼框架。
文|楊亮 網(wǎng)易云商高級 Java 開發(fā)工程師
一、什么是 SPI?
SPI(Service Provider Interface)是一種旨在由第三方實現(xiàn)或者擴展的 API。它可以用于啟用、擴展甚至替換框架中的組件。SPI 的目的是為了在不修改原來的代碼庫的基礎(chǔ)上,開發(fā)人員可以使用新的插件或者模塊來增強框架功能。如我們常使用的 JDBC,在 Java 的核心類庫中,并沒有規(guī)定開發(fā)者需要使用何種類型的數(shù)據(jù)庫,開發(fā)者可以根據(jù)自身需求來選擇不同的數(shù)據(jù)庫類型,可以是 MySQL、Oracle。
所以?Java 的核心類庫只提供了數(shù)據(jù)庫驅(qū)動的接口 Java.sql.Driver,不同的數(shù)據(jù)庫服務(wù)提供商可以實現(xiàn)此接口,而開發(fā)者只需配置相應(yīng)數(shù)據(jù)庫驅(qū)動的實現(xiàn)類,JDBC 框架就能自行加載第三方的服務(wù)以達到客戶端訪問不同類型的數(shù)據(jù)庫的功能。
在很多主流的開發(fā)框架中,我們都可以看到 SPI 的身影,除了 JDK 提供的 SPI 機制外,還有諸如 Spring、Spring cloud Alibaba Dubbo 等等,接下來筆者將介紹如何使用它們及其實現(xiàn)原理。
二、JDK SPI
?(一)案例?
-
定義接口規(guī)范
-
定義接口實現(xiàn)類
-
配置文件
在 resources 目錄下添加純文本文件 META-INF/services/com.demo.jdkspi.api.SayHelloService, 內(nèi)容如下:
-
編寫測試類
客戶端引入依賴,并使用 ServiceLoader 加載接口:
運行結(jié)果如下:
(二) JDK SPI 原理解析?
通過案例我們可以知道?JDK SPI 機制主要是通過 ServiceLoader 來實現(xiàn)的,需要注意的是,實現(xiàn)類的加載是一種懶加載機制,創(chuàng)建 ServiceLoader 并不會去加載接口實現(xiàn),而是在遍歷的時候再去加載。
創(chuàng)建 ServiceLoader 實例流程:
主要流程描述
獲取線程上下文的 ClassLoader:由于 ServiceLoader 是在 rt.jar 下的,而接口實現(xiàn)類是在 classpath 下面,這打破了雙親委派模型,所以需要從線程上下文中獲取 AppClassLoader 用于加載目標接口及其實現(xiàn)類。
清空 providers 緩存:清空歷史加載緩存。
創(chuàng)建 LazyIterator,后續(xù)遍歷所有實現(xiàn)類的時候會使用此迭代器。
加載目標服務(wù)流程:
主要流程描述
在迭代器開始遍歷前,SayHelloService 會去加載 ClassPath(由前文提到的 AppClassLoader 決定的)下所有的目標接口的配置信息。
接口實現(xiàn)類的實例化主要是先通過 Class.forName 創(chuàng)建一個 Class 對象,然后通過反射創(chuàng)建實例。
在實現(xiàn)類實例化后,ServiceLoader 會根據(jù)實現(xiàn)類的全限定名為標識將實例緩存起來。
?(三)JDK SPI 總結(jié)?
優(yōu)點:
-
解耦:JDK SPI 使得第三方服務(wù)模塊加載控制的邏輯與調(diào)用者的業(yè)務(wù)代碼分離,從而實現(xiàn)解耦。
-
懶加載:在創(chuàng)建 ServiceLoader 實例的時候并不會去加載第三方服務(wù)模塊,而是在遍歷的時候去加載。
缺點
-
只能通過遍歷的方式去獲取所有的接口實現(xiàn)類,并沒有實現(xiàn)按需加載。
-
如果接口實現(xiàn)類依賴了其他擴展實現(xiàn),JDK SPI 并沒有實現(xiàn)依賴注入的功能。
三、Spring SPI
Spring Boot Starter 是一種依賴的集合,它使得我們只需要進行簡單的配置就能獲取 Spring 和相關(guān)技術(shù)的一站式服務(wù)。而 Spring Boot Starter 的實現(xiàn)也離不開 SPI 思想,下面我們通過實現(xiàn)一個簡單的 starter 組件來體會一下它的魅力。
?(一)Spring Boot Starter 案例?
-
編寫 SayHelloService 的實現(xiàn)類及 Spring 配置類
創(chuàng)建一個獨立的項目 greeter-spring-boot-starter,并編寫 SayHelloService?實現(xiàn)類及 Spring 配置類
-
配置文件
在 resources/META-INF 目錄下創(chuàng)建 spring.factories 文件,內(nèi)容如下:???????
-
引入依賴
在客戶端項目中引用 greeter-spring-boot-starter 依賴???????
-
效果展示
在客戶端 Spring 項目啟動的時候,可以清楚的看見,我們編寫的 Greeter 會被 Spring IoC 容器加載。
?(二)Spring Boot Starter 原理解析?
在 Spring SPI 中,也有一個類似于 ServiceLoader 的類——SpringFactoriesLoader,在 Spring 容器啟動的時候,會通過 SpringFactoriesLoader 去“META-INF/spring.factories”獲取配置類信息,然后將這些配置類信息封裝成 BeanDefinition,這樣 Spring IoC 容器就能管理這些 Bean 了,主要流程如下:
主要流程描述:
SpringFactoriesLoader 加載配置類信息發(fā)生在構(gòu)建 SpringApplication 實例的時候,SpringFactoriesLoader 會讀取“META-INF/spring.factories”下的配置信息并緩存起來。
AutoConfigurationImportSelector 是在 @EnableAutoConfiguration 中引入的, AutoConfigurationImportSelector 的核心功能是:獲取“org.springframework.boot.autoconfigure.EnableAutoConfiguration”的配置類列表,并且會篩選一遍(如我們在 @EnableAutoConfiguration 中配置了 exclude 屬性),得到最終需要加載的配置類列表。
ConfigurationClassPostProcessor 會將最終需要加載的配置類列表并將其加載為 BeanDefinition,后續(xù)在解析 BeanClass 的時候,也會調(diào)用 Class.forName 來獲取配置類的 Class 對象。Spring Bean 的裝載流程本文不再贅述。
?(三)Spring SPI 總結(jié)?
通過將第三方服務(wù)實現(xiàn)類交給 Spring 容器管理,很好解決了 JDK SPI 沒有實現(xiàn)依賴注入的問題。
配合 Spring Boot 條件裝配,可以在一定條件下實現(xiàn)按需加載第三方服務(wù),而不是加載所有的擴展點實現(xiàn)。
四、Dubbo SPI
SPI 機制在 Dubbo 中也有所應(yīng)用,Dubbo 通過 SPI 機制加載所有的組件,只不過 Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強。在 Dubbo 源碼中,經(jīng)常能看到如下代碼,它們分別是指定名稱擴展點,激活擴展點和自適應(yīng)擴展點:???????
ExtensionLoader.getExtensionLoader(XXX.class).getExtension(name);ExtensionLoader.getExtensionLoader(XXX.class).getActivateExtension();ExtensionLoader.getExtensionLoader(XXX.class).getAdaptiveExtension(url,key);Dubbo SPI 的相關(guān)邏輯都封裝在了 ExtensionLoader 類中,通過 ExtensionLoader 我們可以加載指定的實現(xiàn)類,Dubbo 的 SPI 擴展有兩個規(guī)則:
需要在 resources 目錄下創(chuàng)建任意目錄結(jié)構(gòu):META-INF/dubbo、META-INF/dubbo/internal、META-INF/services 在對應(yīng)的目錄下創(chuàng)建以接口全路徑名命名的文件。
文件內(nèi)容是 Key 和 Value 形式的數(shù)據(jù),Key 是一個字符串,Value 是一個對應(yīng)擴展點的實現(xiàn)。
?(一)指定名稱擴展點?
案例
-
聲明擴展點接口
在一個依賴了 Dubbo 框架的工程中,創(chuàng)建一個擴展點接口及一個實現(xiàn),擴展點接口需要使用 @SPI 注解,代碼如下:???????
-
配置文件
在 resources 目錄下添加純文本文件 META-INF/dubbo/com.spi.api.dubbo.SayHelloService,內(nèi)容如下:???????
-
編寫測試類
?(二)激活擴展點?
有些時候一個擴展點可能有多個實現(xiàn),我們希望獲取其中的某一些實現(xiàn)類來實現(xiàn)復(fù)雜的功能,Dubbo 為我們定義了 @Activate 注解來標注實現(xiàn)類,表明該擴展點為激活擴展點。其中 Dubbo Filter 是我們平時常用的激活擴展點。
案例
在服務(wù)提供者端實現(xiàn)兩個功能,一個是在服務(wù)調(diào)用的時候打印調(diào)用日志,第二個是檢查系統(tǒng)狀態(tài),如果系統(tǒng)未就緒,則直接返回報錯。
-
定義打印日志的 filter
-
定義系統(tǒng)狀態(tài)檢查的filter???????
-
配置文件
在 resources 目錄下添加純文本文件 META-INF/dubbo/com.alibaba.dubbo.rpc.Filter,內(nèi)容如下:???????
-
執(zhí)行效果
在服務(wù)提供者端,執(zhí)行目標方法之前,會先去執(zhí)行我們定義的兩個 Filter,效果如圖所示:
?(三)自適應(yīng)擴展點?
自適應(yīng)擴展點就是能根據(jù)上下文動態(tài)匹配一個擴展類,有時候有些擴展并不想在框架啟動階段被加載,而是希望在擴展方法被調(diào)用時,根據(jù)運行時參數(shù)進行加載。
案例
-
定義自適應(yīng)擴展點接口
-
定義擴展點實現(xiàn)類
-
配置文件
在 resources 目錄下添加純文本文件 META-INF/dubbo/com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt,內(nèi)容如下:???????
-
編寫測試類
?(四)Dubbo 擴展點原理分析?
獲取 ExtensionLoader 實例
ExtensionLoader.getExtensionLoader 這個方法主要返回一個 ExtensionLoader 實例,主要邏輯如下:
先從緩存“EXTENSION_LOADERS”中獲取擴展類對應(yīng)的實例;
如果緩存未命中,則創(chuàng)建一個新的實例,保存在 EXTENSION_LOADERS 中;
在ExtensionLoader構(gòu)造方法中,會初始化一個ExtensionFactory;
獲取擴展點方法 getExtension
先從緩存 cachedClasses 中獲取擴展類,如果沒有就從 META-INF/dubbo/internal/ 、META-INF/dubbo/、META-INF/services/三個目錄中加載。
獲取到擴展類以后,檢查緩存 EXTENSION_INSTANCES 中是否有該擴展類的實現(xiàn),如果沒有就通過反射實例化后放入緩存中。
實現(xiàn)依賴注入,如果當前實例依賴了其他擴展實現(xiàn),那么 Dubbo 會將依賴注入到當前實例中。
將擴展類實例通過 Wrapper 裝飾器進行包裝。
以上步驟中,第一個步驟是加載擴展類的關(guān)鍵,第三和第四個步驟是 Dubbo IoC 與 AOP 的具體實現(xiàn)。其中依賴注入是通過調(diào)用 injectExtension 來實現(xiàn)的且只支持 setter 方式的注入。
獲取自適應(yīng)擴展點方法 getAdaptiveExtension
調(diào)用 getAdaptiveExtensionClass 方法獲取自適應(yīng)擴展 Class 對象。
通過反射進行實例化。調(diào)用 injectExtension 方法向擴展類實例中注入依賴。
雖然上述三個流程和和普通擴展點的獲取方法類似,但是在處理 Class 對象的時候,Dubbo 會動態(tài)生成自適應(yīng)擴展點的動態(tài)代理類,然后使用 javassist(默認)編譯源碼,得到代理類 Class 實例。其中動態(tài)生成的自適應(yīng)擴展類的源碼如下(以上述代碼中的 SimpleAdaptiveExt 為例):
package com.spi.impl.dubbo.adaptive;import org.apache.dubbo.common.extension.ExtensionLoader;public class SimpleAdaptiveExt$Adaptive implements com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt { public void sayHello(org.apache.dubbo.common.URL arg0, java.lang.String arg1) { if (arg0 == null) throw new IllegalArgumentException("url == null"); org.apache.dubbo.common.URL url = arg0; String extName = url.getParameter("serviceKey", "default"); if(extName == null) throw new IllegalStateException("Failed to get extension (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt) name from url (" + url.toString() + ") use keys([serviceKey])"); com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt extension = (com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt)ExtensionLoader.getExtensionLoader(com.spi.impl.dubbo.adaptive.SimpleAdaptiveExt.class).getExtension(extName); extension.sayHello(arg0, arg1); }}從上述代碼中我們可以看到,在方法 SayHello 中,會去獲取 url 中 serviceKey 對應(yīng)的值,如果有就使用該值對應(yīng)的擴展點實現(xiàn),否則使用默認的擴展點實現(xiàn)。
?(五)Dubbo SPI 總結(jié)?
Dubbo 的擴展點加載從 JDK SPI 擴展點發(fā)現(xiàn)機制加強而來,并且改進了 JDK?SPI 的以下問題:
JDK SPI 會一次性實例化擴展點所有實現(xiàn),而 Dubbo 可以使用自適應(yīng)擴展點,在擴展方法調(diào)用的時候再實例化。
增加了對 IoC 的支持,一個擴展點可以通過 setter 方式來注入其他擴展點。
增加了 AOP 的支持,基于 Wrapper 包裝器類來增強原有擴展類實例。
五、多租戶系統(tǒng)中定制技術(shù)結(jié)合 SPI 展望
多租戶系統(tǒng)中動態(tài)個性化配置與定制技術(shù)能滿足不同租戶的個性化要求,但是大量的定制任務(wù)可能使系統(tǒng)變得十分復(fù)雜。
為了方便管理及維護不同租戶的個性化配置,結(jié)合 SPI 可以使用不同擴展實現(xiàn)來啟用或擴展框架中的組件的思想,我們可以設(shè)計一個租戶個性化定制管理平臺,該平臺能管理各個租戶的定制化配置,開發(fā)人員將不同租戶的個性化差異抽象為一個個的定制點,定制管理平臺能收集并管理這些定制點信息,業(yè)務(wù)系統(tǒng)在運行時能從定制平臺中獲取租戶的個性化配置并加載相應(yīng)的擴展實現(xiàn),從而滿足不同租戶的個性化需求。整體架構(gòu)如下:
租戶個性化定制管理平臺主要功能及特性如下:
抽象定制點:開發(fā)人員將租戶特征抽象成不同的定制點接口,對于不同特征的租戶有不同的擴展實現(xiàn)。
定制點發(fā)現(xiàn):每個服務(wù)的定制點及實現(xiàn)信息需要上報給定制管理平臺。
定制租戶個性化配置:運營人員可以根據(jù)租戶的特征配置不同的定制點實現(xiàn)。
動態(tài)加載:在租戶訪問業(yè)務(wù)系統(tǒng)的具體服務(wù)時,業(yè)務(wù)系統(tǒng)能從管理平臺中獲取到相應(yīng)租戶的配置信息,并且可以通過責任鏈/裝飾器模式來組裝一個或者多個定制點實現(xiàn)。
租戶隔離:運營人員為租戶設(shè)置好個性化配置后,定制管理平臺能夠?qū)⑴渲眯畔⒁宰鈶舻木S度存儲,從而實現(xiàn)不同租戶定制內(nèi)容的隔離。
定制復(fù)用:對租戶共有特征進行重用配置或者對那些沒有配置的租戶采用默認配置。
租戶個性化定制管理平臺可以將租戶個性化特征以元數(shù)據(jù)的方式進行管理,后續(xù)只要新租戶的個性化需求能通過現(xiàn)有定制點的元數(shù)據(jù)進行描述,那么只需要修改配置的方式來滿足新需求,即使?jié)M足不了,也只需要新增或者實現(xiàn)定制點接口并且上報給定制管理平臺,這使得系統(tǒng)易于維護,代碼復(fù)用性也會更高。
參考資料?
《Dubbo 2.7 開發(fā)指南》
《Spring Cloud Alibaba 微服務(wù)原理與實戰(zhàn)》
?作者介紹?
楊亮,網(wǎng)易云商高級 Java 開發(fā)工程師,負責云商平臺公共業(yè)務(wù)模塊和內(nèi)部中間件的設(shè)計與開發(fā)。
總結(jié)
以上是生活随笔為你收集整理的JDK、Spring、Dubbo SPI 原理介绍的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大数据洞察画像自动化实践
- 下一篇: 实时通信服务中的语音解混响算法实践