總結/朱季謙
本文主要記錄我對Dubbo SPI實現原理的理解,至於什麼是SPI,我這裡就不像其他博文一樣詳細地從概念再到Java SPI細細分析了,直接開門見山來分享我對Dubbo SPI的見解。
Dubbo SPI的機制比較類似Spring IOC的getBean()載入,當傳入一個存在的beanName,就可以返回該beanName對應的物件。同理,在Dubbo SPI中,我們同樣傳入一個存在的name,Dubbo框架會自動返回該key對應的物件。不難猜測,Dubbo SPI與Spring IOC在底層應該都有一個大致相似的邏輯,簡單的說,就是兩者都可通過beanName或key值,到框架裡的某個map快取當中,找到對應的class類名,然後對該class類名進行反射生成物件,初始化完成該物件,最後返回一個完整的物件。然而,在這個過程當中,Spirng相對來說會更復雜,它裡面還有一堆後置處理器......
簡單舉一個例子,大概解釋一下Dubbo SPI實現原理,然後再進一步分析原始碼。
首先,我在org.apache.dubbo.test目錄下,定義一個@SPI註解到介面:
package org.apache.dubbo.test;
import org.apache.dubbo.common.extension.SPI;
@SPI("dog")
public interface Animal {
void haveBehavior();
}
然後,在同一個目錄下,建立兩個實現該介面的類,分別為Dog,Cat。
Dog——
package org.apache.dubbo.test;
public class Dog implements Animal {
@Override
public void haveBehavior() {
System.out.println("狗會叫");
}
}
Cat——
package org.apache.dubbo.test;
public class Cat implements Animal {
@Override
public void haveBehavior() {
System.out.println("貓會抓老鼠");
}
}
注意看,Animal介面的類名為org.apache.dubbo.test.Animal,接下來,我們在resource目錄的/META_INF/dubbo需新建一個對應到該介面名的File檔案,檔名與Animal介面的類名一致:org.apache.dubbo.test.Animal。之所以兩者名字要一致,是因為這樣只需拿到Animal介面的類名,到resource目錄的/META_INF/dubbo,就可以通過該類名,定位到與Animal介面相對應的檔案。
在Dubbo中,檔名org.apache.dubbo.test.Animal的檔案裡,其實存了類似Spring bean那種的資料,即id對應bean class的形式——
cat=org.apache.dubbo.test.Cat
dog=org.apache.dubbo.test.Dog
這兩行資料,分別是Animal介面的實現類Cat和Dog的class全限名。
整個的目錄結構是這樣的——
最後寫一個測試類DubboSPITestTest演示一下效果——
package org.apache.dubbo.test;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.jupiter.api.Test;
class DubboSPITestTest {
@Test
public void test(){
Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog");
dog.haveBehavior();
Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
cat.haveBehavior();
}
}
執行結果如下——
先簡單捋一下這個思路是怎麼實現的,ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog")這行程式碼內部,會根據Animal介面完整名org.apache.dubbo.test.Animal找到某個指定目錄下的同名File檔案org.apache.dubbo.test.Animal,然後按行迴圈解析檔案裡的內容,以key-value形式載入到某個map快取裡。
類似這樣操作(當然,原始碼裡會更復雜些)——
Map<String,String> map = new HashMap<>();
map.put("cat","org.apache.dubbo.test.Cat");
map.put("dog","org.apache.dubbo.test.Dog");
當然,真實原始碼裡,value存的是已根據類名得到的Class,但其實無論是類名還是Class,最後都是可以反射生成物件的,
這時候,就可以根據執行程式碼去動態獲取到該介面對應的實現類了。例如,需要使用的是org.apache.dubbo.test.Cat這個實現類,那麼在呼叫getExtension("cat")方法中,我們傳入的引數是"cat",就會從剛剛解析檔案快取的map中,根據map.get("cat")拿到對應org.apache.dubbo.test.Cat。既然能拿到類名了,不就可以通過反射的方式生成該類的物件了嗎?當然,生成的物件裡可能還有屬性需要做注入操作,這就是Dubbo IOC的功能,這塊會在原始碼分析裡進一步說明。當物件完成初始化後,就會返回生成的物件指向其介面參照Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog")。
整個過程,就是可根據程式碼去動態獲取到某個介面的實現類,方便靈活呼叫的同時,實現了介面和實現類的解耦。
Dubbo SPI是在Java SPI基礎上做了拓展,Java SPI中與介面同名的檔案裡,並不是key- value的形式,純粹是按行來直接存放各實現類的類名,例如這樣子——
org.apache.dubbo.test.Cat
org.apache.dubbo.test.Dog
這就意味著,Java SPI在實現過程中,通過介面名定位讀取到resource中介面同名檔案時,是無法做到去選擇性地根據某個key值來選擇某個介面的實現類,它只能全部讀取,再全部迴圈獲取到對應介面實現類呼叫相應方法。這就意味著,可能存在一些並非需要呼叫的實現類,也會被載入並生成物件一同返回來,無法做到按需獲取。
因此,Dubbo在原有基礎上,則補充了Java SPI無法按需通過某個key值去呼叫指定的介面實現類,例如上面提到的,Dubbo SPI可通過cat這個key,去按需返回對應的org.apache.dubbo.test.Cat類的實現物件。
下面就來分析一下具體實現的原理細節,以下程式碼做案例。
Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
cat.haveBehavior();
先來分析ExtensionLoader.getExtensionLoader(Animal.class)方法——
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
//判斷傳進來引數是否空
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
//判斷是否為介面
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
//判斷是否具有@SPI註解
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
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;
}
在這個方法裡,若傳進來的Class引數為空或者非介面或者沒有@SPI註解,都會丟擲一個IllegalArgumentException異常,說明傳進來的Class必須需要滿足非空,為介面,同時具有@SPI註解修飾,才能正常往下執行。我們在這裡傳進來的是Animal.class,它滿足了以上三個條件。
@SPI("cat")
public interface Animal {
void haveBehavior();
}
接下來,在最後這部分程式碼裡,主要是建立一個ExtensionLoader物件。
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物件,該物件包括了兩個東西,一個是type,一個是objectFactory。這兩個東西在後面原始碼裡都會用到。
建立完ExtensionLoader物件後,就會開始呼叫getExtension方法——
Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
進入到getExtension("cat")方法當中,內部會呼叫另一個過載方法getExtension(name, true)。
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();
}
//step 1
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
//step 2
//雙重檢查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
//建立擴充套件範例
instance = createExtension(name, wrap);
//設定範例到holeder中
holder.set(instance);
}
}
}
return (T) instance;
}
該方法主要有兩部分。
step 1,先從快取裡查詢name為「cat」的物件是否存在,即呼叫getOrCreateHolder(name),在該方法裡,會去cachedInstances快取裡查詢。cachedInstances是一個定義為ConcurrentMap<String, Holder>的map快取。若cachedInstances.get(name)返回null的話,說明快取裡還沒有name對應的物件資料,那麼就會建立一個key值為name,value值為new Holder<>()的鍵值對快取。
private Holder<Object> getOrCreateHolder(String name) {
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
return holder;
}
想必你一定會有疑惑,為什麼這裡要建立一個new Holder<>()物件呢?
進到Holder類裡,就會發現,其內部用private修飾封裝一個泛型變數value,這就意味著,外部類是無法修改該value值,能起到一個封裝保護的作用。我們正在通過name='cat'去得到一個org.apache.dubbo.test.Cat實現類物件,該物件若能正常生成,最後就會封裝到一個Holder物件裡,再將Holder物件存放到cachedInstances快取裡。
public class Holder<T> {
private volatile T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
因此,就會有該從快取裡獲取Holder的操作——
//根據name為「cat」去快取裡查詢封裝了org.apache.dubbo.test.Cat物件的Holder物件。
final Holder<Object> holder = getOrCreateHolder(name);
//若能查詢到,就從Holder物件取出內部封裝的物件
Object instance = holder.get();
若holder.get()得到的物件為null,說明還沒有生成該「cat」對應的org.apache.dubbo.test.Cat類物件。
那麼,就會繼續往下執行——
//雙重檢查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
//建立擴充套件範例
instance = createExtension(name, wrap);
//設定範例到holeder中
holder.set(instance);
}
}
}
這裡用到了一個雙重檢查的操作,避免在多執行緒情況裡出現某一個執行緒建立了一半,另一個執行緒又開始建立同樣物件,就會出現問題。
這行instance = createExtension(name, wrap)程式碼,主要實現的功能,就是得到「cat」對應的org.apache.dubbo.test.Cat類物件,然後將返回的物件通過holder.set(instance)封裝在Holder物件裡。
private T createExtension(String name, boolean wrap) {
// step 1從組態檔中載入所有的擴充套件類,可得到"設定項名稱"到"設定類"的對映關係表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//step 2 將得到的clazz通過反射建立範例物件。
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
//step 3 對範例物件的屬性做依賴注入,即Dubbo IOC邏輯。
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)) {
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));
}
}
}
}
initExtension(instance);
//step 4 返回初始化完成的物件
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
createExtension(String name, boolean wrap)方法裡主要實現了以下
step 1 從組態檔中載入所有的擴充套件類,可得到"設定項名稱"到"設定類"的對映關係表。
step 2 將得到的clazz通過反射建立範例物件。
step 3 對範例物件的屬性做依賴注入,即Dubbo IOC邏輯。
step 4 返回初始化完成的物件
一、先來看第一步的程式碼分析——
// step 1從組態檔中載入所有的擴充套件類,可得到"設定項名稱"到"設定類"的對映關係表
Class<?> clazz = getExtensionClasses().get(name);
其中getExtensionClasses()方法是獲取返回一個解析完介面對應Resource裡檔案的Map<String, Class>快取,程式碼最後部分get(name)在這個案例裡,就是根據「cat」獲得「org.apache.dubbo.test.Cat」的Class。方法內部cachedClasses.get()返回的這個Map> classes正是存放了介面對應Resource檔案裡key- value資料,即car=org.apache.dubbo.test.Cat和dog=org.apache.dubbo.test.Dog這類。
private Map<String, Class<?>> getExtensionClasses() {
//從快取中獲取已載入的拓展類:car=org.apache.dubbo.test.Cat
Map<String, Class<?>> classes = cachedClasses.get();
//雙重檢查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
當然,首次呼叫cachedClasses.get()返回值classes肯定為null。這裡在classes==null時,同樣使用了一個雙重檢查的操作,最後會去呼叫loadExtensionClasses()方法,該方法主要做的一件事,就是去讀取到Resource中介面對應的檔案進行解析,然後將解析的資料以key-value快取到Map<String, Class<?>>裡,最後通過cachedClasses.set(classes)存入到cachedClasses裡,這裡的cachedClasses同樣是一個final定義的Holder物件,作用與前文提到的一致,都是封裝在內部以private修飾,防止被外部類破壞。
主要看下loadExtensionClasses()內部邏輯——
private Map<String, Class<?>> loadExtensionClasses() {
//step 1 對SPI註解進行解析,獲取SPI預設對value
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
/**
* step 2 strategies包含以下四種策略,代表查詢四種不同目錄下的檔案:
*
* DubboInternalLoadingStrategy 表示目錄"META-INF/dubbo/internal/"
* DubboExternalLoadingStrategy 表示目錄""META-INF/dubbo/external/""
* DubboLoadingStrategy 表示目錄"META-INF/dubbo/"
* ServicesLoadingStrategy 表示目錄"META-INF/services/"
*/
for (LoadingStrategy strategy : strategies) {
//載入指定資料夾下對組態檔,找到SPI預設對value的class
//apache
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
//alibaba
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
首先,執行cacheDefaultExtensionName()方法,該方法是對介面修飾的@SPI進行解析,獲取註解裡的value值。例如,在該例子裡,Animal的註解@SPI("cat"),那麼,通過cacheDefaultExtensionName()方法,即能獲取到註解@SPI裡的預設值「cat」。之所以獲取該註解的值,是用來當做預設值,即如果沒有傳入指定需要獲取的name,那麼就返回cat對應的類物件。
接著,就是遍歷四種不同目錄,查詢是否有與介面Animal對應的檔案。這裡的strategies是一個陣列,裡面包含四種物件,每個物件代表查詢某個目錄,包括"META-INF/dubbo/internal/"、"META-INF/dubbo/external/"、"META-INF/dubbo/"、"META-INF/services/",表示分別到這四種目錄去檢視是否有滿足的檔案。
for迴圈裡呼叫了兩次loadDirectory方法,群分就是一個是查詢apache版本的,一個是查詢alibaba版本的,兩方法底層其實都是一樣,只需要關注其中一個實現即可。
/**
* step 2 strategies包含以下四種策略,代表查詢四種不同目錄下的檔案:
*
* DubboInternalLoadingStrategy 表示目錄"META-INF/dubbo/internal/"
* DubboExternalLoadingStrategy 表示目錄"META-INF/dubbo/external/"
* DubboLoadingStrategy 表示目錄"META-INF/dubbo/"
* ServicesLoadingStrategy 表示目錄"META-INF/services/"
*/
for (LoadingStrategy strategy : strategies) {
//載入指定資料夾下對組態檔,找到SPI預設對value的class
//apache
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
//alibaba
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
在loadDirectory方法裡,就是定位到介面對應的File檔案,獲取檔案的路徑,然後呼叫loadResource方法對檔案進行解析——
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
//資料夾路徑+type 全限定名:META-INF/dubbo/internal/org.apache.dubbo.test.Animal
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls = null;
ClassLoader classLoader = findClassLoader();
// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader.getResources(fileName);
}
}
if (urls == null || !urls.hasMoreElements()) {
if (classLoader != null) {
//根據檔名載入讀取所有同名檔案
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
//載入資源進行解析
loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
loadResource方法主要是讀取File檔案資源,然後迴圈遍歷檔案裡的每一行記錄,跳過開頭為#的註釋記錄,對cat=org.apache.dubbo.test.Cat形式的行記錄進行切割。通過這行程式碼int i = line.indexOf('=')定位到等於號=的位置,然後以name = line.substring(0, i).trim()來擷取等於號前面的字串作為key, 以 line = line.substring(i + 1).trim()擷取等於號=後面的字串作為value,形成key-value鍵值對形式資料,進一步傳到 loadClass方法進行相應快取。
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;
//按行迴圈讀取設定內容
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) {
//以等於號 = 為界,讀取key健 value值
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
//載入類,通過loadClass對類進行快取
loadClass(extensionClasses, resourceURL, Class.forName(line, 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);
}
}
以cat=org.apache.dubbo.test.Cat資料為例子,debug可以看到,最後解析得到的為——
最後,到loadClass看一下是怎麼對從檔案裡解析出來的key-value資料進行快取,注意一點是,在執行該方法時,已將上文拿到的line="org.apache.dubbo.test.Cat"通過Class.forName(line, true, classLoader)生成了對應的Class。
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);
} else if (isWrapperClass(clazz)) {
cacheWrapperClass(clazz);
} else {
//程式進入此分支,表明class是一個普通的拓展類
//檢測class是否有預設的構造方法,如果沒有,則丟擲異常
clazz.getConstructor();
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);
}
}
//names = ["cat"]
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);
}
}
}
}
這裡只需要關注最後的saveInExtensionClass方法,可以看到,最後將從檔案裡解析出來的「cat」-->org.apache.dubbo.test.Cat存入到Map<String, Class<?>> extensionClasses快取當中。
這個Map<String, Class<?>> extensionClasses快取是在loadExtensionClasses()方法裡建立的,該loadExtensionClasses方法最後會將extensionClasses進行返回。
private Map<String, Class<?>> loadExtensionClasses() {
......
//建立用來快取儲存解析檔案裡key-value資料
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
......
return extensionClasses;
}
到這一步,就完成了Animal介面對應的resource/META-INF/dubbo/org.apache.dubbo.test.Animal檔案的解析,解析出來的資料存放到了extensionClasses這個Map快取裡。
回顧先前的方法呼叫,可以看到,最終得到extensionClasses的map快取,會返回到getExtensionClasses()方法,因此,在createExtension呼叫getExtensionClasses().get(name),就相當於是呼叫extensionClasses.get(name)。因為傳到方法裡的引數name="cat",故而返回的Class即org.apache.dubbo.test.Cat。
接著往下執行,到程式碼EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance())就是通過clazz.newInstance()反射建立了一個暫時還是空屬性的物件,同時快取到EXTENSION_INSTANCES快取裡,這是一個ConcurrentMap<Class<?>, Object>快取,避免反覆進行反射建立物件。
範例化完成org.apache.dubbo.test.Cat物件的建立,接下來就是通過injectExtension(instance)對物件進行依賴注入了。主要功能就類似Spring IOC裡bean存在@Resource或者@Autowired註解的屬性時,該bean在範例化建立完物件後,需要對屬性進行補充,即將@Resource或者@Autowired註解的屬性通過反射的方式,指向另外的bean物件。在Dubbo IOC裡,同樣是類似的實現。首先,會過濾掉那些非setXxx()的方法,只對setXxx()方法處理。這種處理方式,就是擷取set後面的字元,例如,存在這樣一個setHitInformService (HitInformService hitInformService)方法,那麼就會擷取set後面的字元,並且對擷取後的第一個字元做小寫處理,得到「hitInformService」。注意一點是,同時,會獲取引數裡的型別HitInformService,如果型別為陣列、String、Boolean、Character、Number、Date其中一個,則不會對其進行注入操作。反之,就會繼續往下執行。
/**
* Dubbo IOC目前僅支援setter方式注入
* @param instance
* @return
*/
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
//遍歷目標類的所有方法
for (Method method : instance.getClass().getMethods()) {
//檢測方法是否以set開頭,且方法僅有一個引數,且方法存取級別為public
if (!isSetter(method)) {
continue;
}
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
//獲取setter方法引數型別
Class<?> pt = method.getParameterTypes()[0];
//判斷該物件是否為陣列、String、Boolean、Character、Number、Date型別,若是,則跳出本次迴圈,繼續下一次迴圈
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
//獲取屬性名,比如 setName方法對應屬性名name
String property = getSetterProperty(method);
/**
* objectFactory 變數的型別為 AdaptiveExtensionFactory,
* AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 列表,用於儲存其他型別的 ExtensionFactory。
*
* Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。
* 前者用於建立自適應的拓展,後者是用於從 Spring 的 IOC 容器中獲取所需的拓展。
*
*/
//從ObjectFactory中獲取依賴物件
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
//通過反射呼叫setter方法依賴
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
執行 Object object = objectFactory.getExtension(pt, property)到行程式碼,就是去獲取HitInformService hitInformService參照對應的物件,這裡獲取有兩種方式,一種是通過SpringExtensionFactory去通過getBean(name)走Spring載入bean的方式獲取物件,另一種是通過本文的Dubbo SPI方式,根據name去解析檔案裡對應的介面實現類Class反射生成返回。
無論是通過哪種方式,最後都需獲取返回一個物件,然後通過method.invoke(instance, object)反射去執行對應的setXxx()方法,將物件進行屬性注入到前文SPI建立的物件cat裡。
到這裡,就完成了介面Animal對應cat這個實現類的建立了,這個過程,就是Dubbo SPI的底層實現細節。最後,將得到的org.apache.dubbo.test.Cat物件向上指向其介面Animal參照,通過介面就可以呼叫該實現類重寫的haveBehavior方法了。
Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
cat.haveBehavior();