dubbo之SPI

方袁發表於2019-01-19

1、SPI簡介

SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的本質是將介面實現類的全限定名配置在檔案中,並由服務載入器讀取配置檔案,載入實現類。這樣可以在執行時,動態為介面替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程式提供擴充功能。SPI 機制在第三方框架中也有所應用,比如 Dubbo 就是通過 SPI 機制載入所有的元件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模組。基於 SPI,我們可以很容易的對 Dubbo 進行擴充。如果大家想要學習 Dubbo 的原始碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然後再來分析 Dubbo SPI 的原始碼。

2、SPI示例

2.1 Java SPI示例

本節通過一個示例演示 Java SPI 的使用方法。首先,我們定義一個介面,名稱為 HelloService。

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。檔案內容為實現類的全限定的類名,如下:

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);
    }
}

最後結果如下:

Java SPI
Hello, I am A
Hello, I am B

從測試結果可以看出,我們的兩個實現類被成功的載入,並輸出了相應的內容。關於 Java SPI 的演示先到這裡,接下來演示 Dubbo SPI。

2.2 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 的用法:

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();
    }
}

測試結果如下:

Java SPI
Hello, I am A
Hello, I am B

3、Dubbo SPI原始碼解析

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()方法主要是利用反射原理,根據類的許可權定名載入成類,並存入快取中

相關文章