1.簡介
SPI 全稱為 Service Provider Interface崩溪,是一種服務發(fā)現(xiàn)機制。SPI 的本質(zhì)是將接口實現(xiàn)類的全限定名配置在文件中斩松,并由服務加載器讀取配置文件伶唯,加載實現(xiàn)類。這樣可以在運行時惧盹,動態(tài)為接口替換實現(xiàn)類乳幸。正因此特性,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能钧椰。SPI 機制在第三方框架中也有所應用粹断,比如 Dubbo 就是通過 SPI 機制加載所有的組件。不過嫡霞,Dubbo 并未使用 Java 原生的 SPI 機制瓶埋,而是對其進行了增強,使其能夠更好的滿足需求秒际。在 Dubbo 中悬赏,SPI 是一個非常重要的模塊÷玻基于 SPI闽颇,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學習 Dubbo 的源碼寄锐,SPI 機制務必弄懂兵多。接下來尖啡,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然后再來分析 Dubbo SPI 的源碼剩膘。
需要特別說明的是衅斩,本篇文章以及本系列其他文章所分析的源碼版本均為 dubbo-2.7.6。因此大家在閱讀文章的過程中怠褐,需注意將代碼版本切換到 dubbo-2.7.6 tag 上畏梆。
2.SPI 示例
2.1 Java SPI 示例
前面簡單介紹了 SPI 機制的原理,本節(jié)通過一個示例演示 Java SPI 的使用方法奈懒。首先奠涌,我們定義一個接口,名稱為 Robot磷杏。
public interface Robot {
void sayHello();
}
接下來定義兩個實現(xiàn)類溜畅,分別為 OptimusPrime 和 Bumblebee。
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
接下來 META-INF/services 文件夾下創(chuàng)建一個文件极祸,名稱為 Robot 的全限定名 org.apache.spi.Robot慈格。文件內(nèi)容為實現(xiàn)類的全限定的類名,如下:
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee
做好所需的準備工作遥金,接下來編寫代碼進行測試浴捆。
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
serviceLoader.forEach(Robot::sayHello);
}
}
最后來看一下測試結(jié)果,如下:
從測試結(jié)果可以看出稿械,我們的兩個實現(xiàn)類被成功的加載汤功,并輸出了相應的內(nèi)容。關(guān)于 Java SPI 的演示先到這里溜哮,接下來演示 Dubbo SPI滔金。
2.2 Dubbo SPI 示例
Dubbo 并未使用 Java SPI,而是重新實現(xiàn)了一套功能更強的 SPI 機制茂嗓。Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中餐茵,通過 ExtensionLoader,我們可以加載指定的實現(xiàn)類述吸。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下忿族,配置內(nèi)容如下。
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現(xiàn)類配置不同蝌矛,Dubbo SPI 是通過鍵值對的方式進行配置道批,這樣我們可以按需加載指定的實現(xiàn)類。另外入撒,在測試 Dubbo SPI 時隆豹,需要在 Robot 接口上標注 @SPI 注解。下面來演示 Dubbo SPI 的用法:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
測試結(jié)果如下:
Dubbo SPI 除了支持按需加載接口實現(xiàn)類茅逮,還增加了 IOC 和 AOP 等特性璃赡,這些特性將會在接下來的源碼分析章節(jié)中一一進行介紹判哥。
3. Dubbo SPI 源碼分析
上一章簡單演示了 Dubbo SPI 的使用方法。我們首先通過 ExtensionLoader 的 getExtensionLoader 方法獲取一個 ExtensionLoader 實例碉考,然后再通過 ExtensionLoader 的 getExtension 方法獲取拓展類對象塌计。這其中,getExtensionLoader 方法用于從緩存中獲取與拓展類對應的 ExtensionLoader侯谁,若緩存未命中锌仅,則創(chuàng)建一個新的實例。該方法的邏輯比較簡單墙贱,本章就不進行分析了技扼。下面我們從 ExtensionLoader 的 getExtension 方法作為入口,對拓展類對象的獲取過程進行詳細的分析嫩痰。
@SuppressWarnings("unchecked")
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 獲取默認的拓展實現(xiàn)類
return getDefaultExtension();
}
// Holder,顧名思義窍箍,用于持有目標對象
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 雙重檢查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 創(chuàng)建拓展實例
instance = createExtension(name);
// 設(shè)置實例到 holder 中
holder.set(instance);
}
}
}
// 返回獲取拓展類對象
return (T) instance;
}
上面代碼的邏輯比較簡單串纺,首先檢查緩存,緩存未命中則創(chuàng)建拓展對象椰棘。下面我們來看一下創(chuàng)建拓展對象的過程是怎樣的纺棺。
private T createExtension(String name) {
// 1、從配置文件中加載所有的拓展類邪狞,可得到“配置項名稱”到“配置類”的映射關(guān)系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 2祷蝌、通過反射創(chuàng)建實例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 3、向?qū)嵗凶⑷胍蕾嚕╯etter方式)
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
// 4帆卓、循環(huán)創(chuàng)建 Wrapper 實例
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
// 將當前instance作為參數(shù)傳給 Wrapper的構(gòu)造方法巨朦,并通過反射創(chuàng)建 Wrapper 實例。
// 然后向 Wrapper 實例中注入依賴剑令,最后將 Wrapper 實例再次賦值給 instance 變量
instance = injectExtension((T)wrapperClass.getConstructor(type)
.newInstance(instance));
}
}
// 5糊啡、Lifecycle類型初始化實例
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
createExtension 方法的邏輯稍復雜一下,包含了如下的步驟:
- 通過 getExtensionClasses 獲取所有的拓展類
- 通過反射創(chuàng)建拓展對象
- 向拓展對象中注入依賴
- 將拓展對象包裹在相應的 Wrapper 對象中
- Lifecycle類型實例初始化
以上步驟中吁津,第一個步驟是加載拓展類的關(guān)鍵棚蓄,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現(xiàn)。在接下來的章節(jié)中碍脏,將會重點分析 getExtensionClasses 方法的邏輯梭依,以及簡單介紹 Dubbo IOC 的具體實現(xiàn)。
3.1 獲取所有的拓展類
我們在通過名稱獲取拓展類之前典尾,首先需要根據(jù)配置文件解析出拓展項名稱到拓展類的映射關(guān)系表(Map<名稱, 拓展類>)役拴,之后再根據(jù)拓展項名稱從映射關(guān)系表中取出相應的拓展類即可。相關(guān)過程的代碼分析如下:
private Map<String, Class<?>> getExtensionClasses() {
// 從緩存中獲取已加載的拓展類
Map<String, Class<?>> classes = cachedClasses.get();
// 雙重檢查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加載拓展類
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
這里也是先檢查緩存钾埂,若緩存未命中扎狱,則通過 synchronized 加鎖侧到。加鎖后再次檢查緩存,并判空淤击。此時如果 classes 仍為 null匠抗,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯污抬。
/**
* synchronized in getExtensionClasses
* */
private Map<String, Class<?>> loadExtensionClasses() {
// 1汞贸、SPI 注解進行解析
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// 2、加載指定文件夾下的配置文件(策略模式)
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.excludedPackages());
}
return extensionClasses;
}
loadExtensionClasses 方法總共做了兩件事情印机,一是對 SPI 注解進行解析矢腻,二是調(diào)用 loadDirectory 方法加載指定文件夾配置文件。SPI 注解解析過程比較簡單射赛,無需多說多柑。下面我們來看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst, String... excludedPackages) {
// fileName = 文件夾路徑 + type 全限定名
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls = null;
ClassLoader classLoader = findClassLoader();
// 嘗試首先從ExtensionLoader的類加載器加載
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader.getResources(fileName);
}
}
if(urls == null || !urls.hasMoreElements()) {
// 根據(jù)文件名加載所有的同名文件
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, excludedPackages);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " + type + ", description file: " + fileName + ").", t);
}
}
loadDirectory 方法先通過 classLoader 獲取所有資源鏈接楣责,然后再通過 loadResource 方法加載資源竣灌。我們繼續(xù)跟下去,看一下 loadResource 方法的實現(xiàn)秆麸。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, String... excludedPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
// 按行讀取配置內(nèi)容
while ((line = reader.readLine()) != null) {
// 定位 # 字符
final int ci = line.indexOf('#');
if (ci >= 0) {
// 截取 # 之前的字符串初嘹,# 之后的內(nèi)容為注釋,需要忽略
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();
line = line.substring(i + 1).trim();
}
if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
// 加載類屯烦,并通過 loadClass 方法對類進行緩存
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} 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 方法用于讀取和解析配置文件,并通過反射加載類房铭,最后調(diào)用 loadClass 方法進行其他操作驻龟。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) 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)) {
// 設(shè)置 cachedAdaptiveClass緩存
cacheAdaptiveClass(clazz);
// 檢測 clazz 是否是 Wrapper 類型
} else if (isWrapperClass(clazz)) {
// 存儲 clazz 到 cachedWrapperClasses 緩存中
cacheWrapperClass(clazz);
// 程序進入此分支缸匪,表明 clazz 是一個普通的拓展類
} else {
// 檢測 clazz 是否有默認的構(gòu)造方法迅脐,如果沒有,則拋出異常
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);
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 如果類上有 Activate 注解,則使用 names 數(shù)組的第一個元素作為鍵龟梦,
// 存儲 name 到 Activate 注解對象的映射關(guān)系
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 存儲 Class 到名稱的映射關(guān)系
cacheName(clazz, n);
// 存儲名稱到 Class 的映射關(guān)系
saveInExtensionClass(extensionClasses, clazz, n);
}
}
}
}
如上隐锭,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass计贰、cachedWrapperClasses 和 cachedNames 等等钦睡。除此之外,該方法沒有其他什么邏輯了躁倒。
到此荞怒,關(guān)于緩存類加載的過程就分析完了洒琢。整個過程沒什么特別復雜的地方,大家按部就班的分析即可褐桌,不懂的地方可以調(diào)試一下衰抑。接下來,我們來聊聊 Dubbo IOC 方面的內(nèi)容荧嵌。
3.2 Dubbo IOC
Dubbo IOC 是通過 setter 方法注入依賴呛踊。Dubbo 首先會通過反射獲取到實例的所有方法,然后再遍歷方法列表啦撮,檢測方法名是否具有 setter 方法特征谭网。若有,則通過 ObjectFactory 獲取依賴對象赃春,最后通過反射調(diào)用 setter 方法將依賴設(shè)置到目標對象中愉择。整個過程對應的代碼如下:
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
// 遍歷目標類的所有方法
for (Method method : instance.getClass().getMethods()) {
// 檢測方法是否以 set 開頭,且方法僅有一個參數(shù)织中,且方法訪問級別為 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 方法參數(shù)類型
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 獲取屬性名锥涕,比如 setName 方法對應屬性名 name
String property = getSetterProperty(method);
// 從 ObjectFactory 中獲取依賴對象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通過反射調(diào)用 setter 方法設(shè)置依賴
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;
}
在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory抠璃,AdaptiveExtensionFactory 內(nèi)部維護了一個 ExtensionFactory 列表,用于存儲其他類型的 ExtensionFactory脱惰。Dubbo 目前提供了兩種 ExtensionFactory搏嗡,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于創(chuàng)建自適應的拓展拉一,后者是用于從 Spring 的 IOC 容器中獲取所需的拓展采盒。這兩個類的類的代碼不是很復雜,這里就不一一分析了蔚润。
Dubbo IOC 目前僅支持 setter 方式注入磅氨,總的來說,邏輯比較簡單易懂嫡纠。
3.參考資料
本文參考于Dubbo官網(wǎng)烦租,詳情以官網(wǎng)最新文檔為準。