Dubbo之SPI機制的實現(xiàn)原理和優(yōu)勢分析
Dubbo中SPI機制的實現(xiàn)原理和優(yōu)勢
確保系統(tǒng)的擴展性是我們開展架構(gòu)設計工作的核心目標之一。實現(xiàn)擴展性的方法有很多,JDK 本身內(nèi)置了一個 SPI(Service Provider Interface,服務提供者接口)機制,來幫開發(fā)人員動態(tài)加載各種不同的實現(xiàn)類,只要這些實現(xiàn)類遵循一定的開發(fā)規(guī)范即可。
另一方面,JDK 自帶的 SPI 機制存在一定的缺陷,因此市面上有些框架對 JDK 中的 SPI 機制做了一些增強,這方面的代表性框架就是 Dubbo。
JDK 中的 SPI 機制解析
如果我們采用 JDK 中的 SPI,具體的開發(fā)工作會涉及三個步驟。
對于 SPI 的開發(fā)者而言,我們需要設計一個服務接口,然后根據(jù)業(yè)務場景提供不同的實現(xiàn)類,這是第一步。
接下來的第二步是關(guān)鍵,我們需要創(chuàng)建一個以服務接口命名的配置文件,并把這個文件放置到代碼工程的 META-INF/services 目錄下。請注意,在這個配置文件中,我們需要指定服務接口對應實現(xiàn)類的完整類名。通過這一步,我們可以得到了一個包含 SPI 類和配置的 jar 包。
最后,SPI 的使用者就可以加載這個 jar 包并找到其中的這個配置文件,并根據(jù)所配置的實現(xiàn)類完整類名對這些類進行實例化。
上圖中的后面兩個步驟實際上都是為了遵循 JDK 中 SPI 的實現(xiàn)機制而進行的配置工作。
為了實現(xiàn)對 SPI 實現(xiàn)類的動態(tài)記載,JDK 專門提供了一個 ServiceLoader 工具類,這個工具類的使用方法如下所示:
public static void main(String[] args) { ServiceLoader<LogProvider> loader = ServiceLoader.load(LogProvider.class); for (LogProvider provider : loader) { System.out.println(provider.getClass()); provider.info(“testInfo”); } }
這里有一個 LogProvider 接口,并通過 ServiceLoader 的 load 方法將這個接口所配置的實現(xiàn)類加載到內(nèi)存中,從而可以方便地使用這些 SPI 實現(xiàn)類所提供的功能。
接下來,讓我們來分析一下這個 ServiceLoader 工具類的實現(xiàn)原理。
ServiceLoader 本身實現(xiàn)了 JDK 中的 Iterable 接口,因此在上面的代碼示例中,通過 ServiceLoader.load 方法我們獲取的是一個迭代器,而底層則用到了 ServiceLoader.LazyIterator 這個迭代器類。
從命名上看,LazyIterator 是一個具備延遲加載機制的迭代器,它有 hasNextService 和 nexServicet 這兩個核心方法。我們先來看 hasNextService 方法:
//配置文件路徑 static final String PREFIX = "META-INF/services/"; private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { // 通過 PREFIX 前綴與服務接口的名稱,我們可以找到目標 SPI 配置文件 String fullName = PREFIX + service.getName(); // 加載配置文件 if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } // 對 SPI 配置文件進行遍歷,并解析配置內(nèi)容 while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 解析配置文件 pending = parse(service, configs.nextElement()); } // 更新 nextName 字段 nextName = pending.next(); return true; }
可以看到,hasNextService 方法的核心作用是找到并解析配置文件。而接下來要展開的 nextService 方法則負責對所配置的類進行實例化,核心實現(xiàn)如下所示:
private S nextService() { String cn = nextName; nextName = null; // 加載 nextName 字段指定的類 Class<?> c = Class.forName(cn, false, loader); // 檢測類型 if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } // 創(chuàng)建實現(xiàn)類的對象 S p = service.cast(c.newInstance()); // 緩存已創(chuàng)建的對象 providers.put(cn, p); return p; }
這里通過 newInstance 方法創(chuàng)建了目標實例,并將已創(chuàng)建的實例對象放到 providers 集合中進行緩存,從而提高訪問效率。
Dubbo 中的 SPI 機制解析
為了實現(xiàn)框架自身的擴展性,Dubbo 也采用了類似 JDK 中 SPI 的設計思想,但提供了一套新的實現(xiàn)方式,并添加了一些擴展功能。
Dubbo 中與 SPI 機制相關(guān)的注解主要包括@SPI、@Adaptive 和@Activate,其中@SPI 注解提供了與 JDK 中 SPI 類似的功能。
這三個注解的應用場景各不相同,其中@SPI 注解為 Dubbo 提供了最基礎(chǔ)的 SPI 機制,而@Adaptive 和@Activate 注解都是構(gòu)建在這個注解之上,因此我們重點介紹@SPI 注解。如果在某個接口上添加了這個注解,那么 Dubbo 在運行過程中就會去查找接口對應的擴展點實現(xiàn)。
在 Dubbo 中,隨處可以看到@SPI 注解的應用場景。
舉個例子,Protocol 接口定義如下:
@SPI("dubbo")public interface Protocol
可以看到,在這個接口上使用的就是@SPI(“dubbo”) 注解。
請注意,在@SPI 注解中可以指定默認擴展點的名稱,例如這里的“dubbo”用來表明在 Protocol 接口的所有實現(xiàn)類中,DubboProtocol 是它的默認實現(xiàn)。
有了 SPI 的定義,我們接下來看一看 Dubbo 中 SPI 配置信息的存儲方式。我們已經(jīng)知道,JDK 只會把 SPI 配置存放在 META-INF/services/這個目錄下,而 Dubbo 則提供了三個類似這樣的目錄:
作為示例,我們繼續(xù)圍繞上面提到的 Protocol 接口展開討論。
針對 Protocol 接口,Dubbo 提供了 gRPCProtocol、DubboProtocol 等多個實現(xiàn)類,并通過 SPI 機制完成對具體某種實現(xiàn)方案的加載過程。
讓我們分別來到提供這些實現(xiàn)類的代碼工程 dubbo-rpc-grpc 和 dubbo-rpc-dubbo,會發(fā)現(xiàn)在 META-INF/dubbo/internal/目錄下都包含了一個 com.apache.dubbo.rpc.Protocol 配置文件。其中,dubbo-rpc-grpc 工程的代碼結(jié)構(gòu)如圖所示:
類似的,dubbo-rpc-dubbo 工程的代碼結(jié)構(gòu)如下圖所示:
我們分別打開這兩個工程的 com.apache.dubbo.rpc.Protocol 配置文件,可以發(fā)現(xiàn)它們分別指向了 org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol 和 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 類。
- //dubbo-rpc-grpc 工程:
grpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol
- //dubbo-rpc-dubbo 工程:
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
當 Dubbo 在引用具體某一個代碼工程時,就可以通過這個工程中的配置項就可以找到 Dubbo 接口對應的擴展點實現(xiàn)。
同時,我們從上面配置項中也可以看出,Dubbo 中采用的定義方式與 JDK 中的不一樣。Dubbo 使用的一個 Key 值(如上面的 gRPC 和 Dubbo)來指定具體的配置項名稱,而不是采用完整類路徑。
介紹完@SPI 注解,我們接下來看 Dubbo 中的 ExtensionLoader 類,這個類扮演著與 JDK 中 ServiceLoader 工具類相同的角色。
ExtensionLoader 是實現(xiàn)擴展點加載的核心類,如果我們想要獲取 DubboProtocol 這個實現(xiàn)類,那么可以采用以下方式:
DubboProtocol dubboProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);
我們來看一下這里 getExtension 方法的細節(jié),這個方法代碼如下所示:
public T getExtension(String name) { ... //從緩存中獲取目標對象 Holder<Object> holder = cachedInstances.get(name); if (holder == null) { //將目標對象放到緩存中 cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { //創(chuàng)建目標對象 instance = createExtension(name); holder.set(instance); } } } return (T) instance; }
我們看到這里同樣用到了緩存機制。這個方法會首先檢查緩存中是否已經(jīng)存在擴展點實例,如果沒有則通過 createExtension 方法進行創(chuàng)建。
我們一路跟蹤 createExtension 方法,終于看到了熟悉的 SPI 機制,如下所示:
private Map<String, Class<?>> loadExtensionClasses() { final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { //確定緩存名稱 } Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); //分別從三個目錄中加載類實例 loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadFile(extensionClasses, DUBBO_DIRECTORY); loadFile(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; }
在這里,我們調(diào)用了三次 loadFile 方法,分別在 META-INF/dubbo/、META-INF/services/和 META-INF/dubbo/internal/這三個目錄中加載擴展點。在 loadFile 方法中,Dubbo 是直接通過 Class.forName 的形式加載這些 SPI 的擴展類,并進行緩存。
到這里,我們發(fā)現(xiàn),為了提升實例類的加載速度,Dubbo 和 JDK 都采用了緩存機制,這是它們的一個共同點。但實際上,我們也已經(jīng)可以梳理 Dubbo 中 SPI 機制與 JDK 中 SPI 機制的區(qū)別,核心有兩點,就是 配置文件位置和 獲取實現(xiàn)類的條件。
從加載 SPI 實例的配置文件位置來看,Dubbo 支持更多的加載路徑。JDK 只能加載一個固定的 META-INF/services,而 Dubbo 有三個路徑。
就獲取實現(xiàn)類的條件而言,Dubbo 采用的是直接通過名稱對應的 Key 值來定位具體實現(xiàn)類,而 ServiceLoader 內(nèi)部使用的是一個迭代器,在獲取目標接口的實現(xiàn)類時,只能通過遍歷的方式把配置文件中的類全部加載并實例化,顯然這樣效率比較低下。
簡單來說,Dubbo 沒有直接沿用 JDK SPI 機制,而是自己實現(xiàn)一套的主要目的就是克服這種效率低下的情況,并提供了更多的靈活性。
總結(jié)
從 Dubbo 配置項的定義中發(fā)現(xiàn),Dubbo 采用了與 JDK 不同的實現(xiàn)機制。雖然 Dubbo 也采用了 SPI 機制,也是從 jar 包中動態(tài)加載實現(xiàn)類,但它的實現(xiàn)方式與 JDK 中基于 ServiceLoader 是不一樣的。于是,詳細分析了 JDK 和 Dubbo 在 SPI 機制設計和實現(xiàn)上的差異,并闡明了 Dubbo 內(nèi)部的實現(xiàn)原理和所具備的優(yōu)勢。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java利用openoffice將doc、docx轉(zhuǎn)為pdf實例代碼
這篇文章主要介紹了Java利用openoffice將doc、docx轉(zhuǎn)為pdf實例代碼,分享了相關(guān)代碼示例,小編覺得還是挺不錯的,具有一定借鑒價值,需要的朋友可以參考下2018-01-01Java8并發(fā)新特性CompletableFuture
這篇文章主要介紹了Java8并發(fā)新特性CompletableFuture,CompletableFuture針對Future接口做了改進,相比Callable/Runnable接口它支持多任務進行鏈式調(diào)用、組合、多任務并發(fā)處理,下面文章更多相關(guān)內(nèi)容得介紹,需要的小伙伴可以參考一下2022-06-06簡述springboot及springboot cloud環(huán)境搭建
這篇文章主要介紹了簡述springboot及springboot cloud環(huán)境搭建的方法,包括spring boot 基礎(chǔ)應用環(huán)境搭建,需要的朋友可以參考下2017-07-07SpringBoot+SpringSecurity實現(xiàn)基于真實數(shù)據(jù)的授權(quán)認證
Spring Security是一個功能強大且高度可定制的身份驗證和訪問控制框架,Spring Security主要做兩個事情,認證、授權(quán)。這篇文章主要介紹了SpringBoot+SpringSecurity實現(xiàn)基于真實數(shù)據(jù)的授權(quán)認證,需要的朋友可以參考下2021-05-05Hibernate中Session.get()方法和load()方法的詳細比較
今天小編就為大家分享一篇關(guān)于Hibernate中Session.get()方法和load()方法的詳細比較,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03