1.簡介
SPI 全稱為 Service Provider Interface,是 Java 提供的一種服務發現機制。SPI 的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類。這樣可以在運作時,動态為接口替換實作類。正是以特性,我們可以很容易的通過 SPI 機制為我們的程式提供拓展功能。SPI 機制在第三方架構中也有所應用,比如 Dubbo 就是通過 SPI 機制加載所有的元件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的子產品。如果大家想要學習 Dubbo 的源碼,SPI 機制務必弄懂。下面,我們先來了解一下 Java SPI 與 Dubbo SPI 的使用方法,然後再來分析 Dubbo SPI 的源碼。
2.SPI 示例
2.1 Java SPI 示例
前面簡單介紹了 SPI 機制的原理,本節通過一個示例來示範 JAVA SPI 的使用方法。首先,我們定義一個接口,名稱為 Robot。
public interface Robot {
void sayHello();
}
接下來定義兩個實作類,分别為擎天柱 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 檔案夾下建立一個檔案,名稱為 Robot 的全限定名 com.tianxiaobo.spi.Robot。檔案内容為實作類的全限定的類名,如下:
com.tianxiaobo.spi.OptimusPrime
com.tianxiaobo.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);
}
}
最後來看一下測試結果,如下:

從測試結果可以看出,我們的兩個實作類被成功的加載,并輸出了相應的内容。關于 Java SPI 的示範先到這,接下來示範 Dubbo SPI。
2.2 Dubbo SPI 示例
Dubbo 并未使用 Java SPI,而是重新實作了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實作類。Dubbo SPI 的實作類配置放置在 META-INF/dubbo 路徑下,下面來看一下配置内容。
optimusPrime = com.tianxiaobo.spi.OptimusPrime
bumblebee = com.tianxiaobo.spi.Bumblebee
與 Java SPI 實作類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們就可以按需加載指定的實作類了。另外,在測試 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();
}
}
測試結果如下:
示範完 Dubbo SPI,下面來看看 Dubbo SPI 對 Java SPI 做了哪些改進,以下内容引用至 Dubbo 官方文檔。
- JDK 标準的 SPI 會一次性執行個體化擴充點所有實作,如果有擴充實作初始化很耗時,但如果沒用上也加載,會很浪費資源。
- 如果擴充點加載失敗,連擴充點的名稱都拿不到了。比如:JDK 标準的 ScriptEngine,通過 getName() 擷取腳本類型的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導緻 RubyScriptEngine 類加載失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當使用者執行 ruby 腳本時,會報不支援 ruby,而不是真正失敗的原因。
- 增加了對擴充點 IOC 和 AOP 的支援,一個擴充點可以直接 setter 注入其它擴充點。
在以上改進項中,第一個改進項比較好了解。第二個改進項沒有進行驗證,就不多說了。第三個改進項是增加了對 IOC 和 AOP 的支援,這是什麼意思呢?這裡簡單解釋一下,Dubbo SPI 加載完拓展執行個體後,會通過該執行個體的 setter 方法解析出執行個體依賴項的名稱。比如通過 setProtocol 方法名,可知道目标執行個體依賴 Protocal。知道了具體的依賴,接下來即可到 IOC 容器中尋找或生成一個依賴對象,并通過 setter 方法将依賴注入到目标執行個體中。說完 Dubbo IOC,接下來說說 Dubbo AOP。Dubbo AOP 是指使用 Wrapper 類(可自定義實作)對拓展對象進行包裝,Wrapper 類中包含了一些自定義邏輯,這些邏輯可在目标方法前行前後被執行,類似 AOP。Dubbo AOP 實作的很簡單,其實就是個代理模式。這個官方文檔中有所說明,大家有興趣可以查閱一下。
關于 Dubbo SPI 的示範,以及與 Java SPI 的對比就先這麼多,接下來加入源碼分析階段。
3. Dubbo SPI 源碼分析
上一章,我簡單示範了 Dubbo SPI 的使用方法。我們首先通過 ExtensionLoader 的 getExtensionLoader 方法擷取一個 ExtensionLoader 執行個體,然後再通過 ExtensionLoader 的 getExtension 方法擷取拓展類對象。這其中,getExtensionLoader 用于從緩存中擷取與拓展類對應的 ExtensionLoader,若緩存未命中,則建立一個新的執行個體。該方法的邏輯比較簡單,本章就不就行分析了。下面我們從 ExtensionLoader 的 getExtension 方法作為入口,對拓展類對象的擷取過程進行詳細的分析。
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 擷取預設的拓展實作類
return getDefaultExtension();
}
// Holder 僅用于持有目标對象,沒其他什麼邏輯
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) {
// 建立拓展執行個體,并設定到 holder 中
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則建立拓展對象。下面我們來看一下建立拓展對象的過程是怎樣的。
private T createExtension(String name) {
// 從配置檔案中加載所有的拓展類,形成配置項名稱到配置類的映射關系
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通過反射建立執行個體
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向執行個體中注入依賴
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循環建立 Wrapper 執行個體
for (Class<?> wrapperClass : wrapperClasses) {
// 将目前 instance 作為參數建立 Wrapper 執行個體,然後向 Wrapper 執行個體中注入屬性值,
// 并将 Wrapper 執行個體指派給 instance
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
createExtension 方法的邏輯稍複雜一下,包含了如下的步驟:
- 通過 getExtensionClasses 擷取所有的拓展類
- 通過反射建立拓展對象
- 向拓展對象中注入依賴
- 将拓展對象包裹在相應的 Wrapper 對象中
以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實作。在接下來的章節中,我将會重點分析 getExtensionClasses 方法的邏輯,以及簡單分析 Dubbo IOC 的具體實作。
3.1 擷取所有的拓展類
我們在通過名稱擷取拓展類之前,首先需要根據配置檔案解析出名稱到拓展類的映射,也就是 Map<名稱, 拓展類>。之後再從 Map 中取出相應的拓展類即可。相關過程的代碼分析如下:
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,則加載拓展類。以上代碼的寫法是典型的雙重檢查鎖,前面所分析的 getExtension 方法中有相似的代碼。關于雙重檢查就說這麼多,下面分析 loadExtensionClasses 方法的邏輯。
private Map<String, Class<?>> loadExtensionClasses() {
// 擷取 SPI 注解,這裡的 type 是在調用 getExtensionLoader 方法時傳入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
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("...");
}
// 設定預設名稱,cachedDefaultName 用于加載預設實作,參考 getDefaultExtension 方法
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// 加載指定檔案夾配置檔案
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 注解進行解析,二是調用 loadDirectory 方法加載指定檔案夾配置檔案。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
// fileName = 檔案夾路徑 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
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);
}
}
} catch (Throwable t) {
logger.error("...");
}
}
loadDirectory 方法代碼不多,了解起來不難。該方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作。
private void loadResource(Map<String, Class<?>> extensionClasses,
ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
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) {
// 以 = 為界,截取鍵與值。比如 dubbo=com.alibaba....DubboProtocol
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加載解析出來的限定類名
loadClass(extensionClasses, resourceURL,
Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("...");
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("...");
}
}
loadResource 方法用于讀取和解析配置檔案,并通過反射加載類,最後調用 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("...");
}
if (clazz.isAnnotationPresent(Adaptive.class)) { // 檢測目标類上是否有 Adaptive 注解
if (cachedAdaptiveClass == null) {
// 設定 cachedAdaptiveClass緩存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("...");
}
} else if (isWrapperClass(clazz)) { // 檢測 clazz 是否是 Wrapper 類型
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
// 存儲 clazz 到 cachedWrapperClasses 緩存中
wrappers.add(clazz);
} else { // 程式進入此分支,表明是一個普通的拓展類
// 檢測 clazz 是否有預設的構造方法,如果沒有,則抛出異常
clazz.getConstructor();
if (name == null || name.length() == 0) {
// 如果 name 為空,則嘗試從 Extension 注解擷取 name,或使用小寫的類名作為 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("...");
}
}
// 切分 name
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
// 如果類上有 Activate 注解,則使用 names 數組的第一個元素作為鍵,
// 存儲 name 到 Activate 注解對象的映射關系
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
// 存儲 Class 到名稱的映射關系
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
// 存儲名稱到 Class 的映射關系
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("...");
}
}
}
}
}
如上,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,該方法沒有其他什麼邏輯了,就不多說了。
到此,關于緩存類加載的過程就分析完了。整個過程沒什麼特别複雜的地方,大家按部就班的分析就行了,不懂的地方可以調試一下。接下來,我們來聊聊 Dubbo IOC 方面的内容。
3.2 Dubbo IOC
Dubbo IOC 是基于 setter 方法注入依賴。Dubbo 首先會通過反射擷取到執行個體的所有方法,然後再周遊方法清單,檢測方法名是否具有 setter 方法特征。若有,則通過 ObjectFactory 擷取依賴對象,最後通過反射調用 setter 方法将依賴設定到目标對象中。整個過程對應的代碼如下:
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 周遊目标類的所有方法
for (Method method : instance.getClass().getMethods()) {
// 檢測方法是否以 set 開頭,且方法僅有一個參數,且方法通路級别為 public
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// 擷取 setter 方法參數類型
Class<?> pt = method.getParameterTypes()[0];
try {
// 擷取屬性名
String property = method.getName().length() > 3 ?
method.getName().substring(3, 4).toLowerCase() +
method.getName().substring(4) : "";
// 從 ObjectFactory 中擷取依賴對象
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// 通過反射調用 setter 方法設定依賴
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("...");
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部維護了一個 ExtensionFactory 清單,用于存儲其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于建立自适應的拓展,關于自适應拓展,我将會在下一篇文章中進行說明。SpringExtensionFactory 則是到 Spring 的 IOC 容器中擷取所需拓展,該類的實作并不複雜,大家自行分析源碼,這裡就不多說了。
Dubbo IOC 的實作比較簡單,僅支援 setter 方式注入。總的來說,邏輯簡單易懂。
4.總結
本篇文章簡單介紹了 Java SPI 與 Dubbo SPI 用法與差別,并對 Dubbo SPI 的部分源碼進行了分析。在 Dubbo SPI 中還有一塊重要的邏輯沒有進行分析,那就是 Dubbo SPI 的擴充點自适應機制。該機制的邏輯較為複雜,我将會在下一篇文章中進行分析。好了,其他的就不多說了,本篇檔案就先到這裡了。
本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處
作者:田小波
本文同步釋出在我的個人部落格:
http://www.tianxiaobo.com
本作品采用
知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定進行許可。