Dubbo的SPI擴充套件機制剖析
我們都是知道一個合格的開源框架對於擴充套件的支援都要是相當彈性的,Dubbo 也不例外。Dubbo採用微核心+外掛體系,使得設計優雅,擴充套件性強。Dubbo的擴充套件機制是基於SPI思想來實現的,但是並沒有採用JDK中原生的SPI機制。
1.什麼是SPI
java spi的具體約定為:當服務的提供者,提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄裡同時建立一個以服務介面命名的檔案。該檔案裡就是實現該服務介面的具體實現類。而當外部程式裝配這個模組的時候,就能通過該jar包META-INF/services/裡的配置檔案找到具體的實現類名,並裝載例項化,完成模組的注入。 基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再程式碼裡制定。jdk提供服務實現查詢的一個工具類:java.util.ServiceLoader。
2.為什麼Dubbo自己實現SPI
- JDK中SPI具有很大的缺點,JDK中標準的SPI會一次性例項化擴充套件點所有的實現,不管這些例項化出來的擴充套件點實現有沒有被用到。有的擴充套件點實現初始化時非常的耗時,即使沒有用到也會被載入,這樣就很浪費資源。
- Dubbo的SPI機制中增加了對擴充套件點IOC和AOP的支援,一個擴充套件點可以直接setter注入到其他的擴充套件點中。
3.Dubbo中擴充套件點的概念
Dubbo作用靈活的框架,並不會強制所有使用者都一定使用Dubbo提供的某些架構。例如註冊中心(Registry),Dubbo提供了zk和redis,但是如果我們更傾向於其他的註冊中心的話,我們可以替換掉Dubbo提供的註冊中心。針對這種可被替換的技術實現點我們稱之為擴充套件點
4.微核心架構
微核心架構 (Microkernel architecture) 模式也被稱為外掛架構 (Plugin architecture) 模式。原本與核心整合在一起的元件會被分離出來,核心提供了特定的介面使得這些元件可以靈活的接入,這些元件在核心的管理下工作,但是這些元件可以獨立的發展、更改(不會對現有系統造成改動),只要符合核心的介面即可。典型的例子比如,Eclipse,IDEA 。
5.Dubbo 的微核心設計
Dubbo核心對外暴露出擴充套件點,通過擴充套件點可以實現定製的符合自己業務需求的功能。Dubbo核心通過ExtensionLoader擴充套件點載入器來載入各個SPI擴充套件點。Dubbo 核心對擴充套件是無感的 ,完全不知道擴充套件的存在 ,核心程式碼中不會出現使用具體擴充套件的硬編碼。
術語說明 :
SPI : Service Provider Interface 。
擴充套件點 : 稱 Dubbo 中被 @SPI 註解的 Interface 為一個擴充套件點。
擴充套件 : 被 @SPI 註解的 Interface 的實現稱為這個擴充套件點的一個擴充套件。
6.Dubbo中SPI的約定
擴充套件點約定 : 擴充套件點必須是 Interface 型別 ,必須被 @SPI 註解 ,滿足這兩點才是一個擴充套件點。
擴充套件定義約定:在 META-INF/services/$擴充套件點介面的全類名,META-INF/dubbo/$擴充套件點介面的全類名 , META-INF/dubbo/internal/$擴充套件點介面的全類名 , 這些路徑下定義的檔名稱為 $擴充套件點介面的全類名 , 檔案中以鍵值對的方式配置擴充套件點的擴充套件實現。
預設適應擴充套件 :被 @SPI("abc") 註解的 Interface , 那麼這個擴充套件點的預設適應擴充套件就是 SPI 配置檔案中 key 為 "abc" 的擴充套件。
7.介面實現類配置
通過註解@SPI("dubbo"),預設使用Protocol的實現類DubboProtocol,其實dubbo和實現類DubboProtocol關係類似Spring配置檔案中的id和class的關係,不同的是Dubbo的關係是
配置在目錄/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol檔案中,檔案內容為:
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
簡單來說其實現機制就類似Spring的bean注入,通過key(dubbo、http、hessian)來找到其實現類。
8.擴充套件載入器 ExtensionLoader
擴充套件載入器絕對是一個核心元件了 ,它控制著 dubbo 內部所有擴充套件點的初始化、載入擴充套件的過程。這個類的原始碼是很有必要深入學習的。從 Dubbo 核心設計簡圖可以看到,現在的學習還沒有接觸到 dubbo 的核心。
public class ExtensionLoader<T> {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
// ==============================
private final Class<?> type;
private final ExtensionFactory objectFactory;
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>();
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>();
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();
private volatile Class<?> cachedAdaptiveClass = null;
private String cachedDefaultName;
private volatile Throwable createAdaptiveInstanceError;
private Set<Class<?>> cachedWrapperClasses;
private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<String, IllegalStateException>();
ExtensionLoader 中會儲存兩個靜態屬性 , EXTENSION_LOADERS 儲存了核心開放的擴充套件點對應的 ExtensionLoader 例項物件 (說明了一種擴充套件點有一個對應的 ExtensionLoader 物件)。EXTENSION_INSTANCES 儲存了擴充套件型別 (Class) 和擴充套件型別的例項物件。
type : 被 @SPI 註解的 Interface , 也就是擴充套件點。
objectFactory : 擴充套件工廠,可以從中獲取到擴充套件型別例項物件 ,預設為 AdaptiveExtensionFactory。
cachedNames : 儲存不滿足裝飾模式(不存在只有一個引數,並且引數是擴充套件點型別例項物件的建構函式)的擴充套件的名稱。
cachedClasses : 儲存不滿足裝飾模式的擴充套件的 Class 例項 , 擴充套件的名稱作為 key , Class 例項作為 value。
cachedActivates : 儲存不滿足裝飾模式 , 被 @Activate 註解的擴充套件的 Class 例項。
cachedAdaptiveClass : 被 @Adpative 註解的擴充套件的 Class 例項 。
cachedInstances : 儲存擴充套件的名稱和例項物件 , 副檔名稱為 key , 擴充套件例項為 value。
cachedDefaultName : 擴充套件點上 @SPI 註解指定的預設適配擴充套件。
createAdaptiveInstanceError : 建立適配擴充套件例項過程中丟擲的異常。
cachedWrapperClasses : 滿足裝飾模式的擴充套件的 Class 例項。
exceptions : 儲存在載入擴充套件點配置檔案時,載入擴充套件點過程中丟擲的異常 , key 是當前讀取的擴充套件點配置檔案的一行 , value 是丟擲的異常
9.SPI配置檔案讀取
我們已經知道Dubbo將Protocol的實現類配置到/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol配置檔案中,接下來我們要看的就是將配置檔案中的對應關係解析出來了。其處理操作是在ExtensionLoder的loadFile方法中類來實現的,
簡單來說讀取dubbo=com.alibaba.dubbo.registry.dubbo.DubboRegistryFactory,獲取到鍵dubbo,初始化值com.alibaba.dubbo.registry.dubbo.DubboRegistryFactory然後將對應關係儲存到一個Map中,這樣就可以根據key找到實現類了。
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
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 url = urls.nextElement();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
try {
String line = 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();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
Class<?> clazz = Class.forName(line, true, classLoader);
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else {
try {
clazz.getConstructor(type);
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} catch (NoSuchMethodException e) {
clazz.getConstructor();
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name == null || name.length() == 0) {
if (clazz.getSimpleName().length() > type.getSimpleName().length()
&& clazz.getSimpleName().endsWith(type.getSimpleName())) {
name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
} else {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
}
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
} // end of while read lines
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + url + ") in " + url, t);
}
} // end of while urls
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}