SPI是什麼
SPI是一種簡稱,全名叫 Service Provider Interface,Java自己提供了一套SPI機制,SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類,這樣能夠在運行時,動態爲接口替換實現類,這也是不少框架組件實現擴展功能的一種手段。java
而今天要說的Dubbo SPI機制和Java SPI仍是有一點區別的,Dubbo 並未使用 Java 原生的 SPI 機制,而是對他進行了改進加強,進而能夠很容易地對Dubbo進行功能上的擴展。web
學東西得帶着問題去學,咱們先提幾個問題,再接着看數組
1.什麼是SPI(開頭已經解釋了)緩存
2.Dubbo SPI和Java原生的有什麼區別ruby
3.兩種實現應該如何寫出來微信
Java SPI是如何實現的
先定義一個接口:架構
public interface Car {
void startUp();
}
而後建立兩個類,都實現這個Car接口app
public class Truck implements Car{
@Override
public void startUp() {
System.out.println("The truck started");
}
}
public class Train implements Car{
@Override
public void startUp() {
System.out.println("The train started");
}
}
而後在項目META-INF/services
文件夾下建立一個名稱爲接口的全限定名,com.example.demo.spi.Car
。框架
文件內容寫上實現類的全限定名,以下:編輯器
com.example.demo.spi.Train
com.example.demo.spi.Truck
最後寫一個測試代碼:
public class JavaSPITest {
@Test
public void testCar() {
ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
serviceLoader.forEach(Car::startUp);
}
}
執行完的輸出結果:
The train started
The truck started
Dubbo SPI是如何實現的
Dubbo 使用的SPI並非Java原生的,而是從新實現了一套,其主要邏輯都在ExtensionLoader
類中,邏輯也不難,後面會稍帶講一下
看看使用,和Java的差不了太多,基於前面的例子來看下,接口類須要加上@SPI
註解:
@SPI
public interface Car {
void startUp();
}
實現類不須要改動
配置文件須要放在META-INF/dubbo
下面,配置寫法有些區別,直接看代碼:
train = com.example.demo.spi.Train
truck = com.example.demo.spi.Truck
最後就是測試類了,先看代碼:
public class JavaSPITest {
@Test
public void testCar() {
ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
Car car = extensionLoader.getExtension("train");
car.startUp();
}
}
執行結果:
The train started
Dubbo SPI中經常使用的註解
-
@SPI
標記爲擴展接口 -
@Adaptive
自適應拓展實現類標誌 -
@Activate
自動激活條件的標記
總結一下二者區別:
-
使用上的區別Dubbo使用 ExtensionLoader
而不是ServiceLoader
了,其主要邏輯都封裝在這個類中 -
配置文件存放目錄不同,Java的在 META-INF/services
,Dubbo在META-INF/dubbo
,META-INF/dubbo/internal
-
Java SPI 會一次性實例化擴展點全部實現,若是有擴展實現初始化很耗時,而且又用不上,會形成大量資源被浪費 -
Dubbo SPI 增長了對擴展點 IOC 和 AOP 的支持,一個擴展點能夠直接 setter 注入其它擴展點 -
Java SPI加載過程失敗,擴展點的名稱是拿不到的。好比:JDK 標準的 ScriptEngine,getName() 獲取腳本類型的名稱,若是 RubyScriptEngine 由於所依賴的 jruby.jar 不存在,致使 RubyScriptEngine 類加載失敗,這個失敗緣由是不會有任何提示的,當用戶執行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的緣由
前面的3個問題是否是已經能回答出來了?是否是很是簡單
Dubbo SPI源碼分析
Dubbo SPI使用上是經過ExtensionLoader
的getExtensionLoader
方法獲取一個 ExtensionLoader
實例,而後再經過 ExtensionLoader
的 getExtension
方法獲取拓展類對象。這其中,getExtensionLoader
方法用於從緩存中獲取與拓展類對應的 ExtensionLoader
,若是沒有緩存,則建立一個新的實例,直接上代碼:
public T getExtension(String name) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 獲取默認的拓展實現類
return getDefaultExtension();
}
// 用於持有目標對象
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// DCL
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 建立擴展實例
instance = createExtension(name);
// 設置實例到 holder 中
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 實例中注入依賴,最後將 Wrapper 實例再次賦值給 instance 變量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
這段代碼看着繁瑣,其實也不難,一共只作了4件事情:
1.經過getExtensionClasses
獲取全部配置擴展類
2.反射建立對象
3.給擴展類注入依賴
4.將擴展類對象包裹在對應的Wrapper對象裏面
咱們在經過名稱獲取擴展類以前,首先須要根據配置文件解析出擴展類名稱到擴展類的映射關係表,以後再根據擴展項名稱從映射關係表中取出相應的拓展類便可。相關過程的代碼以下:
private Map<String, Class<?>> getExtensionClasses() {
// 從緩存中獲取已加載的拓展類
Map<String, Class<?>> classes = cachedClasses.get();
// DCL
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加載擴展類
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
這裏也是先檢查緩存,若緩存沒有,則經過一次雙重鎖檢查緩存,判空。此時若是 classes
仍爲 null
,則經過 loadExtensionClasses
加載拓展類。下面是 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("more than 1 default extension name on extension...");
}
// 設置默認名稱,參考 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("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
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) {
// 以等於號 = 爲界,截取鍵與值
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// 加載類,並經過 loadClass 方法對類進行緩存
loadClass(extensionClasses, resourceURL,
Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e =
new IllegalStateException("Failed to load extension class...");
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class...");
}
}
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("...");
}
// 檢測目標類上是否有 Adaptive 註解
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
// 設置 cachedAdaptiveClass緩存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("...");
}
// 檢測 clazz 是不是 Wrapper 類型
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
// 存儲 clazz 到 cachedWrapperClasses 緩存中
wrappers.add(clazz);
// 程序進入此分支,代表 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
等等
到這裏基本上關於緩存類加載的過程就分析完了,其餘邏輯不難,認真地讀下來加上Debug一下都能看懂的。
總結
從設計思想上來看的話,SPI是對迪米特法則和開閉原則的一種實現。
開閉原則:對修改關閉對擴展開放。這個原則在衆多開源框架中都很是常見,Spring的IOC容器也是大量使用。
迪米特法則:也叫最小知識原則,能夠解釋爲,不應直接依賴關係的類之間,不要依賴;有依賴關係的類之間,儘可能只依賴必要的接口。
那Dubbo的SPI爲何不直接使用Spring的呢,這一點從衆多開源框架中也許都能窺探一點端倪出來,由於自己做爲開源框架是要融入其餘框架或者一塊兒運行的,不能做爲依賴被依賴對象存在。
再者對於Dubbo來講,直接用Spring IOC AOP的話有一些架構臃腫,徹底不必,因此本身實現一套輕量級反而是最優解
往期推薦
本文分享自微信公衆號 - 架構技術專欄(jiagoujishu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。