spi 02-spi 的實戰解決 slf4j 包衝突問題java
spi 04-spi dubbo 實現源碼解析github
spi 05-dubbo adaptive extension 自適應拓展apache
而Dubbo中最核心的一點就是SPI和自適應擴展,Dubbo的高擴展性以及其它功能都是在這個基礎上實現的,理解掌握其原理才能看懂後面的一系列功能的實現原理,對咱們平時實現高擴展性也很是有幫助。安全
針對 java 原生 SPI 的不足,dubbo 作了哪些改進呢?併發
咱們能夠一塊兒來看一看。app
須要特別說明的是,本篇文章以及本系列其餘文章所分析的源碼版本均爲 dubbo-2.6.4。
Dubbo 並未使用 Java SPI,而是從新實現了一套功能更強的 SPI 機制。
Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,經過 ExtensionLoader,咱們能夠加載指定的實現類。
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容以下。
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.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 除了支持按需加載接口實現類,還增長了 IOC 和 AOP 等特性,這些特性將會在接下來的源碼分析章節中一一進行介紹。
上一章簡單演示了 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) { // 建立拓展實例 instance = createExtension(name); // 設置實例到 holder 中 holder.set(instance); } } } return (T) instance; }
ps: 這裏使用 DLC 解決了併發安全問題。
上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則建立拓展對象。
下面咱們來看一下建立拓展對象的過程是怎樣的。
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("..."); } }
ps: 這是實現了 IOC
createExtension 方法的邏輯稍複雜一下,包含了以下的步驟:
經過 getExtensionClasses 獲取全部的拓展類
經過反射建立拓展對象
向拓展對象中注入依賴
以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現。
在接下來的章節中,將會重點分析 getExtensionClasses 方法的邏輯,以及簡單介紹 Dubbo IOC 的具體實現。
咱們在經過名稱獲取拓展類以前,首先須要根據配置文件解析出拓展項名稱到拓展類的映射關係表,以後再根據拓展項名稱從映射關係表中取出相應的拓展類便可。
相關過程的代碼分析以下:
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 方法的邏輯。
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; }
ps: 這裏會進行 @SPI
註解的一些判斷和處理。我的感受經過註解,能夠達到更加靈活的控制。
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("..."); } }
ps: 這個和 java 的家在加載很是相似。
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 等等。
除此以外,該方法沒有其餘什麼邏輯了。