SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣能夠在運行時,動態爲接口替換實現類。正所以特性,咱們能夠很容易的經過 SPI 機制爲咱們的程序提供拓展功能。SPI 機制在第三方框架中也有所應用,好比 Dubbo 就是經過 SPI 機制加載全部的組件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了加強,使其可以更好的知足需求。在 Dubbo 中,SPI 是一個很是重要的模塊。基於 SPI,咱們能夠很容易的對 Dubbo 進行拓展。若是你們想要學習 Dubbo 的源碼,SPI 機制務必弄懂。接下來,咱們先來了解一下 Java SPI 與 Dubbo SPI 的用法,而後再來分析 Dubbo SPI 的源碼。java
本節經過一個示例演示 Java SPI 的使用方法。首先,咱們定義一個接口,名稱爲 HelloService。apache
public interface HelloService { void sayHello(); }
家下來定義兩個實現類:HelloAService、HelloBService;緩存
public class HelloAService implements HelloService { @Override public void sayHello() { System.out.println("Hello, I am A"); } } public class HelloBService implements HelloService { @Override public void sayHello() { System.out.println("Hello, I am B"); } }
接下來 META-INF/services 文件夾下建立一個文件,名稱爲 Robot 的全限定名 org.apache.spi.Robot。文件內容爲實現類的全限定的類名,以下:app
org.apache.spi.HelloAService org.apache.spi.HelloBService
作好所需的準備工做,接下來編寫代碼進行測試框架
public class JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class); System.out.println("Java SPI"); serviceLoader.forEach(HelloService::sayHello); } }
最後結果以下:ide
Java SPI Hello, I am A Hello, I am B
從測試結果能夠看出,咱們的兩個實現類被成功的加載,並輸出了相應的內容。關於 Java SPI 的演示先到這裏,接下來演示 Dubbo SPI。學習
Dubbo 並未使用 Java SPI,而是從新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,經過 ExtensionLoader,咱們能夠加載指定的實現類。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容以下。測試
helloAService = org.apache.spi.HelloAService helloBService = org.apache.spi.HelloBService
與 Java SPI 實現類配置不一樣,Dubbo SPI 是經過鍵值對的方式進行配置,這樣咱們能夠按需加載指定的實現類。另外,在測試 Dubbo SPI 時,須要在 Robot 接口上標註 @SPI 註解。下面來演示 Dubbo SPI 的用法:url
public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class); HelloService a = extensionLoader.getExtension("HelloAService"); a.sayHello(); HelloService b = extensionLoader.getExtension("HelloBService"); b.sayHello(); } }
測試結果以下:.net
Java SPI Hello, I am A Hello, I am B
Dubbo SPI相關邏輯都在ExtensionLoader 類中,首先經過getExtensionLoader獲取一個ExtensionLoader實例,而後在根據getExtension獲取type的擴展類。
getExtensionLoader方法比較簡單,先從緩存中獲取,若是緩存不存在,則建立ExtensionLoader對象,並存入緩存中,在看getExtension方法:
public T getExtension(String name) { if (StringUtils.isEmpty(name)) { 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(); // 雙重檢查 if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 根據擴展名建立實例對象 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 (CollectionUtils.isNotEmpty(wrapperClasses)) { 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 + ") could not be instantiated: " + t.getMessage(), t); } }
咱們在經過名稱獲取拓展類以前,首先須要根據配置文件解析出拓展項名稱到拓展類的映射關係表(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; }
getExtensionClasses方法一樣是先從緩存中讀取,緩存不存在,在去加載:
private Map<String, Class<?>> loadExtensionClasses() { // 獲取擴展類SPI註解 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { String[] names = NAME_SEPARATOR.split(value); if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension " + type.getName() + ": " + Arrays.toString(names)); } if (names.length == 1) { cachedDefaultName = names[0]; } } } Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); // 加載指定文件夾下的配置文件 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); return extensionClasses; }
loadExtensionClasses 方法總共作了兩件事情,一是對 SPI 註解進行解析,二是調用 loadDirectory 方法加載指定文件夾配置文件。SPI 註解解析過程比較簡單,無需多說。下面咱們來看一下 loadDirectory 作了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) { // fileName = 文件夾路徑 + type 全限定名 String fileName = dir + type; 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 when load 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(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); exceptions.put(line, e); } } } } finally { reader.close(); } } catch (Throwable t) { logger.error("Exception when load extension class(interface: " + type + ", class file: " + resourceURL + ") in " + resourceURL, t); } }
loadClass()方法主要是利用反射原理,根據類的權限定名加載成類,並存入緩存中