這篇文章内容很幹,做好心理準備!本文詳細講解了 Dubbo SPI 誕生原因以及它的用法,并且詳細解讀了核心類 ExtensionLoader 的關鍵屬性,再根據demo 對 SPI 的加載原理進行詳細解讀。
文章較長,建議收藏!文末有原理圖。
為什麼不使用 JDK SPI
我們接着上次的文章講,既然已經有了 JDK SPI 為什麼還需要 Dubbo SPI 呢?
技術的出現通常都是為了解決現有問題,通過之前的 demo,不難發現 JDK SPI 機制就存在以下一些問題:
- 實作類會被全部周遊并且執行個體化,假如我們隻需要使用其中的一個實作,這在實作類很多的情況下無疑是對機器資源巨大的浪費,
- 無法按需擷取實作類,不夠靈活,我們需要周遊一遍所有實作類才能找到指定實作。
Dubbo SPI 以 JDK SPI 為參考做出了改進設計,進行了性能優化以及功能增強,Dubbo SPI 機制的出現解決了上述問題。除此之外,Dubbo 的 SPI 還支援自适應擴充以及 IOC 和 AOP 等進階特性。
JDK SPI 原理,請移步這篇文章:
提到 Dubbo,為什麼一定要談SPI?|原創
Dubbo SPI配置
Dubbo SPI 的配置做出了改進,在 Dubbo 中有三種不同的目錄可以存放 SPI 配置,用途也不同。
- META-INF/services/ 目錄:此目錄配置檔案用于相容 JDK SPI 。
- META-INF/dubbo/ 目錄:此目錄用于存放使用者自定義 SPI 配置檔案。
- META-INF/dubbo/internal/ 目錄:此目錄用于存放 Dubbo 内部使用的 SPI 配置檔案。
并且配置檔案中可以配置成Key-Value 形式,key 為擴充名,value 為具體實作類。有了擴充名就可以動态根據名字來定位到具體的實作類,并且可以針對性的執行個體化需要的實作類。比如指定 zookeeper,就知道要使用 zookeeper 作為注冊中心的實作。
zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory
Dubbo 并未使用 Java SPI,而是重新實作了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實作類。
除了對應目錄的配置,還需要 @SPI 注解的配合,被修飾的接口表示這是一個擴充接口,@SPI 的 value 表示這個擴充接口的預設擴充實作的擴充名,擴充名就是配置檔案中對應實作類配置資訊的 key。
//dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
@SPI("dubbo")
public interface Protocol {
}
除此之外,還需要 @Adaptive注解配合,被他修飾的類會生成Dubbo的擴充卡,Adaptive注解比較複雜,關于這個注解我們後續文章會再詳細講解。
ExtensionLoader
之前的文章我們提到過,dubbo-common 子產品中的 extension 包包含了 Dubbo SPI 的邏輯,而ExtensionLoader 就位于其中,Dubbo SPI 的核心邏輯幾乎都封裝在 ExtensionLoader 這個類裡,其地位你可以類比為 JDK SPI 中的 java.util.ServiceLoader。
學習 ExtensionLoader 首先了解以下幾個關鍵屬性。
核心靜态變量
1、EXTENSION_LOADERS
// 擴充接口和ExtensionLoader的關系
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
這個字段儲存了擴充接口和ExtensionLoader之間的映射關系,一個擴充接口對應一個ExtensionLoader。key 為接口,value 為 ExtensionLoader。
2、EXTENSION_INSTANCES
//擴充接口實作類和執行個體對象的關系
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
這個字段儲存了擴充接口擴充實作類和執行個體對象之間的關系,key 為擴充實作類,value為執行個體對象。
3、strategies 加載政策
//加載政策
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
import static java.util.ServiceLoader.load;
private static LoadingStrategy[] loadLoadingStrategies() {
return stream(load(LoadingStrategy.class).spliterator(), false)
.sorted().toArray(LoadingStrategy[]::new);
}
strategies 對應的是Dubbo内部實作的三個加載政策,分别對應之前提到的三個不同的SPI配置目錄,接口的繼承關系如下。他們的實作中無非就是包含了兩個資訊:加載目錄和優先級。
每個實作類都繼承了優先級接口Prioritized,是以不同加載政策的加載優先級不同,對應的優先級是:
DubboInternalLoadingStrategy 大于>DubboLoadingStrategy 大于>ServicesLoadingStrateg
對應目錄的優先級分别是:
META-INF/dubbo/internal/>META-INF/dubbo/>META-INF/services/
LoadingStrategy是如何加載的?
需要注意的是,LoadingStrategy的控制了Dubbo内部實作的加載政策,那他自身又是如何加載的呢?
其實根據上面的代碼就可以發現LoadingStrategy正是依賴 JDK SPI 機制來加載的,在loadLoadingStrategies()方法中調用了ServiceLoader.load()方法,從META-INF/services檔案夾下的org.apache.dubbo.common.extension.LoadingStrategy檔案中讀取到了具體實作類并且依次加載。是以一定程度上來說,Dubbo SPI 機制正是依賴于 JDK SPI 機制。
核心成員變量
1、type:與目前 ExtensionLoader 綁定的擴充接口類型。
2、cachedNames:存儲了擴充實作類和擴充名的關系,key 為擴充實作類,value 為對應擴充名。
3、cachedClasses:存儲了擴充名和擴充實作類之間的關系。key 為擴充名,value 為擴充實作類,這個 map 可以和 cachedNames 對應着了解,他們存貯内容是相反的關系,算是一種空間換時間的技巧。
4、cachedInstances:存儲了擴充實作類類名與實作類的執行個體對象之間的關系,key 為擴充實作名,value 為執行個體對象。
5、cachedDefaultName:@SPI 注解配置的預設擴充名。
//擴充接口類型
private final Class<?> type;
//存儲了擴充實作類和擴充名的關系
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
//存儲了擴充名和擴充實作類之間的關系
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
//存儲了擴充實作類類名與執行個體對象之間的關系
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
//擴充接口對@SPI注解配置的value
private String cachedDefaultName;
Dubbo SPI 加載原理
測試 demo
為了友善調試并且抓住核心原理,其實可以跟 JDK SPI 一樣寫一個 demo,我們直接複用上次的擴充接口和實作類,但是這次是用Dubbo SPI 的形式加載。配置檔案需要重新寫一份,寫成key-value形式,并且放在META-INF/dubbo檔案夾下。
/**
* @author 後端開發技術
*/
@SPI
public interface MySPI {
void say();
}
public class HelloMySPI implements MySPI{
@Override
public void say() {
System.out.println("HelloMySPI say:hello");
}
}
public class GoodbyeMySPI implements MySPI {
@Override
public void say() {
System.out.println("GoodbyeMySPI say:Goodbye");
}
}
public static void main(String[] args) {
ExtensionLoader<MySPI> extensionLoader = ExtensionLoader.getExtensionLoader(MySPI.class);
MySPI hello = extensionLoader.getExtension("hello");
hello.say();
MySPI goodbye = extensionLoader.getExtension("goodbye");
goodbye.say();
}
//配置檔案 META-INF/dubbo/org.daley.spi.demo.MySPI
goodbye=org.daley.spi.demo.GoodbyeMySPI
hello=org.daley.spi.demo.HelloMySPI
// 輸出結果
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye
一定記得給接口标記@SPI注解,否則會抛出以下錯誤。
Exception in thread "main" java.lang.IllegalArgumentException: Extension type (interface org.daley.spi.demo.MySPI) is not an extension, because it is NOT annotated with @SPI!
獲得擴充執行個體
在上述 demo 中,我們首先通過 ExtensionLoader 的 getExtensionLoader 方法擷取一個 ExtensionLoader 執行個體,然後再通過 ExtensionLoader 的 getExtension 方法擷取拓展類對象。
在調用getExtensionLoader()時會首先嘗試從EXTENSION_LOADERS中根據擴充點類型獲得ExtensionLoader,如果未命中緩存,則會新建立一個ExtensionLoader,然後傳回。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
……省略校驗邏輯
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
在ExtensionLoader的構造方法中,會綁定擴充點接口類型,并且會綁定擴充對象工廠ExtensionFactory objectFactory,這也是一個擴充點,為了避免越套越深,這裡不做深入追蹤。
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory =
(type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
建立好ExtensionLoader之後,便可以調用getExtension()方法。核心在于獲得擴充實作,首先檢查緩存,緩存未命中則建立擴充對象,然後設定到 holder 中。
public T getExtension(String name) {
return getExtension(name, true);
}
public T getExtension(String name, boolean wrap) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 擷取預設的擴充實作類
return getDefaultExtension();
}
//用于持有目标對象
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
//建立擴充執行個體
instance = createExtension(name, wrap);
//設定到holder中
holder.set(instance);
}
}
}
return (T) instance;
}
建立擴充執行個體方法createExtension()主要包含以下邏輯:
- 通過 getExtensionClasses() 擷取所有的拓展類
- 通過反射建立拓展對象
- 向拓展對象中注入依賴(可以想象到,這裡存在依賴其他SPI擴充點的情況,遞歸注入)
- 将拓展對象包裹在相應的 Wrapper 對象中
- 如果實作了Lifecycle接口,執行生命周期的初始化方法
步驟 1 是加載拓展類的關鍵,步驟 3 和 4 是 Dubbo IOC 與 AOP 的具體實作,注意 IOC 和 AOP 是一種思想,别和 Spring 的 IOC、AOP 混淆。
private T createExtension(String name, boolean wrap) {
//關鍵代碼!從配置檔案中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null || unacceptableExceptions.contains(name)) {
throw findException(name);
}
try {
// 通過反射建立執行個體
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向執行個體中注入依賴
injectExtension(instance);
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
// 循環建立 Wrapper 執行個體
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// 将目前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射建立 Wrapper 執行個體。
// 然後向 Wrapper 執行個體中注入依賴,最後将 Wrapper 執行個體再次指派給 instance 變量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}
//執行生命周期初始化方法 initialize()
initExtension(instance);
return instance;
} catch (Throwable t) {
……
}
}
獲得所有擴充類
我們在根據名稱獲得指定拓展類之前,首先需要根據配置檔案解析出拓展項名稱到拓展類的映射Map(Map<名稱, 拓展類>),之後再根據拓展項名稱從映射關系 Map 中根據 name 取出相應的拓展類即可。
在獲得全部擴充名與擴充類關系classes時,如果還未建立,會有個雙重檢查的過程以防止并發加載。通過檢查後會調用 loadExtensionClasses() 加載擴充類。相關過程的代碼分析如下:
private Map<String, Class<?>> getExtensionClasses() {
// 從緩存中擷取已加載的拓展類
Map<String, Class<?>> classes = cachedClasses.get();
// DBC 雙重檢查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加載所有拓展類邏輯
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
loadExtensionClasses()主要做了兩件事,一是解析@SPI注解設定預設擴充名,二是在這裡會掃描 Dubbo 那三個配置檔案夾下的所有配置檔案,具體注釋如下。
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// 從三種加載政策中獲得擴充實作類,對應加載指定檔案夾下的配置檔案,這裡的加載政策已經根據優先級排好了順序,數字越小越優先
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
strategy.overridden(), strategy.excludedPackages());
// 此處為了相容阿裡巴巴到Apache的版本過度
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
// 設定預設擴充名
private void cacheDefaultExtensionName() {
// 擷取 SPI 注解,這裡的 type 變量是在調用 getExtensionLoader 方法時傳入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
//從@SPI注解獲得預設實作擴充實作名
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 對 SPI 注解内容進行切分
String[] names = NAME_SEPARATOR.split(value);
// 檢測 SPI 注解内容是否合法,不合法則抛出異常
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
// 設定預設名稱,參考 getDefaultExtension 方法
cachedDefaultName = names[0];
}
}
}
可以看出,關鍵的邏輯應該在loadDirectory()方法中。loadDirectory() 方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
String clazz = null;
// 按行讀取配置内容
while ((line = reader.readLine()) != null) {
// 定位 # 字元
final int ci = line.indexOf('#');
if (ci >= 0) {
// 截取 # 之前的字元串,# 之後的内容為注釋,需要忽略
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
// 以等于号 = 為界,截取鍵與值
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
clazz = line.substring(i + 1).trim();
} else {
clazz = line;
}
if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
// 加載類,并通過 loadClass 方法對類進行緩存
loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException(
"Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
loadResource 方法用于讀取和解析配置檔案,并通過反射加載類,最後調用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
// 檢測目标類上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 設定 cachedAdaptiveClass緩存
cacheAdaptiveClass(clazz, overridden);
// 檢測 clazz 是否是 Wrapper 類型
} else if (isWrapperClass(clazz)) {
// 存儲 clazz 到 cachedWrapperClasses 緩存中
cacheWrapperClass(clazz);
} else {
// 程式進入此分支,表明 clazz 是一個普通的拓展類
clazz.getConstructor();
// 檢測 clazz 是否有預設的構造方法,如果沒有,則抛出異常
if (StringUtils.isEmpty(name)) {
// 如果 name 為空,則嘗試從 Extension 注解中擷取 name,或使用小寫的類名作為 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException(
"No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 存儲 Class 到名稱的映射關系
cacheName(clazz, n);
// 存儲名稱到 Class 的映射關系
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}
如上,loadClass 方法操作了不同的緩存,比如解析@Adaptive注解設定到cachedAdaptiveClass緩存;如果是wrapper類設定到cachedWrapperClasses 緩存;儲存拓展類和拓展名到 cachedNames 緩存等等。
到這裡 Dubbo SPI 加載機制的主要邏輯就講完了,彙總了一下核心流程如下圖:
總結
在我們了解了各種實作細節後,最後可以總結一下各種知識點。
- dubbo 的 ExtensionLoader 對應 JDK 的ServiceLoader,包含了加載所需要的各種上下文資訊,一個擴充接口對應一個 ExtensionLoader,關系維護在一個 Map 中。
- 在嘗試擷取拓展類的時候,才開始執行個體化,按需加載,這種懶加載是一種對 JDK SPI 的優化。
- Dubbo SPI 會從三個配置目錄按照優先級加載配置,加載政策是依賴 JDK SPI 加載的。
- 加載配置的時候會加載所有配置的Class類型,然後把拓展名和拓展類的類型維護一個映射(key-value),這也是按需加載,靈活使用的基礎。
如果覺得對你有幫助,歡迎點贊、标或分享!
原文作者:後端開發技術