1. 程式人生 > >【Dubbo原始碼閱讀系列】之 Dubbo SPI 機制

【Dubbo原始碼閱讀系列】之 Dubbo SPI 機制

最近抽空開始了 Dubbo 原始碼的閱讀之旅,希望可以通過寫文章的方式記錄和分享自己對 Dubbo 的理解。如果在本文出現一些紕漏或者錯誤之處,也希望大家不吝指出。

Dubbo SPI 介紹

Java SPI

在閱讀本文之前可能需要你對 Java SPI(Service Provider Interface) 機制有過簡單的瞭解。這裡簡單介紹下:在面向物件的設計中,我們提倡模組之間基於介面程式設計。不同模組可能會有不同的具體實現,但是為了避免模組的之間的耦合過大,我們需要一種有效的服務(服務實現)發現機制來選擇具體模組。SPI 就是這樣一種基於介面程式設計+策略模式+配置檔案,同時可供使用者根據自己的實際需要啟用/替換模組具體實現的方案。

Dubbo SPI 的改進點

以下內容摘錄自 dubbo.gitbooks.io/dubbo-dev-b…
Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。 JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。 如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當用戶執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。 增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點

在 Dubbo 中,如果某個 interface 介面標記了 @SPI 註解,那麼我們認為它是 Dubbo 中的一個擴充套件點。擴充套件點是 Dubbo SPI 的核心,下面我們就擴充套件點載入、擴充套件點自動包裝、擴充套件點自動裝配幾方面來聊聊具體實現。

Dubbo SPI 機制詳解

Dubbo 擴充套件點的載入

在閱讀本文前,如果你閱讀過Java SPI 相關內容,大概能回憶起來有 /META-INF/services 這樣一個目錄。在這個目錄下有一個以介面命名的檔案,檔案的內容為介面具體實現類的全限定名。在 Dubbo 中我們也能找到類似的設計。

  • META-INF/services/(相容JAVA SPI)
  • META-INF/dubbo/(自定義擴充套件點實現)
  • META-INF/dubbo/internal/(Dubbo內部擴充套件點實現)

非常好~我們現在已經知道了從哪裡載入擴充套件點了,再回憶一下,JAVA SPI是如何載入的。

ServiceLoader<DubboService> spiLoader = ServiceLoader.load(XXX.class);
複製程式碼

類似的,在 Dubbo 中也有這樣一個用於載入擴充套件點的類 ExtensionLoader。這一章節,我們會著重瞭解一下這個類到底是如何幫助我們載入擴充套件點的。我們先來看一段簡短的程式碼。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
複製程式碼

在 Dubbo 的實現裡面用到了大量類似的程式碼片段,我們只需要提供一個 type ,即可獲取該 type 的自適應(關於自適應的理解在後文會提到)擴充套件類。在獲取對應自適應擴充套件類時,我們首先獲取該型別的 ExtensionLoader。看到這裡我們應該下意識的感覺到對於每個 type 來說,都應該有一個對應的 ExtensionLoader 物件。我們先來看看 ExtensionLoader 是如何獲取的。

getExtensionLoader()

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
    }
    // 是否被 SPI 註解標識 
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }
    //EXTENSION_LOADERS 為一個 ConcurrentMap集合,key 為 Class 物件,value 為ExtenLoader 物件
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

private ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
複製程式碼

上面這一段的程式碼比較簡單,根據 type 從 EXTENSION_LOADERS 集合中獲取 loader ,如果返回的值為 null 則新建一個 ExtensionLoader 物件。這裡的 objectFactory 獲取也用到了類似的方法,獲取到了 ExtensionFactory 的擴充套件自適應類。

getAdaptiveExtension()

public T getAdaptiveExtension() {
    //cachedAdaptiveInstance用於快取自適應擴充套件類例項
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        // ...
                    }
                }
            }
    }

    return (T) instance;
}
複製程式碼

getAdaptiveExtension()方法用於獲取當前自適應擴充套件類例項,首先會從 cachedAdaptiveInstance 物件中獲取,如果值為 null 同時 createAdaptiveInstanceError 為空,則呼叫 createAdaptiveExtension 方法建立擴充套件類例項。建立完後更新 cachedAdaptiveInstance 。

createAdaptiveExtension()

private T createAdaptiveExtension() {
    try {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        // 省略異常
    }
}
複製程式碼

這裡有兩個方法值得我們關注,injectExtension() 和 getAdaptiveExtensionClass()。injectExtension() 看名字像是一個實現了注入功能的方法,而 getAdaptiveExtensionClass() 則用於獲取具體的自適應擴充套件類。我們依次看下這兩個方法。

injectExtension()

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
                    //如果存在 DisableInject 註解則跳過
                    if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                    }
                    //獲取 method 第一個引數的型別
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}
複製程式碼

簡單的總結下這個方法做了什麼:遍歷當前例項的 set 方法,以 set 方法第四位開始至末尾的字串為關鍵字,嘗試通過 objectFactory 來獲取對應的 擴充套件類實現。如果存在對應擴充套件類,通過反射注入到當前例項中。這個方法相當於完成了一個簡單的依賴注入功能,我們常說 Dubbo 中的 IOC 實際上也是在這裡體現的。

getAdaptiveExtensionClass()

private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
複製程式碼

接著看 getAdaptiveExtensionClass() 方法。首先呼叫 getExtensionClasses() 方法,如果 cachedAdaptiveClass() 不為 null 則返回,如果為 null 則呼叫 createAdaptiveExtensionClass() 方法。依次看下這兩個方法。

getExtensionClasses()

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;
}
複製程式碼

內容比較簡單,我們直接看 loadExtensionClasses() 方法。

private Map<String, Class<?>> loadExtensionClasses() {
    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;
}
複製程式碼

繞來繞去這麼久,終於要進入主題了。為什麼說進入主題了呢?看看這幾個變數的值

  • DUBBO_INTERNAL_DIRECTORY:META-INF/dubbo/internal/
  • DUBBO_DIRECTORY:META-INF/dubbo/
  • SERVICES_DIRECTORY:META-INF/services/

熟悉的配方熟悉的料。。沒錯了,我們馬上就要開始讀取這三個目錄下的檔案,然後開始載入我們的擴充套件點了。

loadDirectory()

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String 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) {
        // ...
    }
}
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;
                        //檔案中的內容以 key=value 的形式儲存,拆分 key 和 vlaue
                        int i = line.indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        // ...
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        // ...
    }
}
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // 用於判斷 class 是不是 type 介面的實現類
    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.");
    }
    // 如果當前 class 被 @Adaptive 註解標記,更新 cachedAdaptiveClass 快取物件
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            // 省略異常
        }
    } else if (isWrapperClass(clazz)) {
    // 這裡涉及到了 Dubbo 擴充套件點的另一個機制:包裝,在後文介紹
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } else {
        clazz.getConstructor();
        // 如果 name 為空,呼叫 findAnnotationName() 方法。如果當前類有 @Extension 註解,直接返回 @Extension 註解value;
        // 若沒有 @Extension 註解,但是類名類似 xxxType(Type 代表 type 的類名),返回值為小寫的 xxx
        if (name == null || name.length() == 0) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            // @Activate 註解用於配置擴充套件被自動啟用條件
            // 如果當前 class 包含 @Activate ,加入到快取中
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                cachedActivates.put(names[0], activate);
            } else {
                // support com.alibaba.dubbo.common.extension.Activate
                com.alibaba.dubbo.common.extension.Activate oldActivate = clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                if (oldActivate != null) {
                    cachedActivates.put(names[0], oldActivate);
                }
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 還記得檔案內容長啥樣嗎?(name = calssvalue),我們最後將其儲存到了 extensionClasses 集合中
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    // ...
                }
            }
        }
    }
}
複製程式碼

這一段程式碼真的相當長啊。。梳理下之後發現其實他做的事情也很簡單:

  1. 拼接生成檔名:dir + type,讀取該檔案
  2. 讀取檔案內容,將檔案內容拆分為 name 和 class 字串
  3. 如果 clazz 類中包含 @Adaptive 註解,將其加入到 cachedAdaptiveClass 快取中
    如果 clazz 類中為包裝類,新增到 wrappers 中
    如果檔案不為 key=class 形式,會嘗試通過 @Extension 註解獲取 name
    如果 clazz 包含 @Activate 註解(相容 com.alibaba.dubbo.common.extension.Activate 註解),將其新增到 cachedActivates 快取中
  4. 最後以 name 為 key ,clazz 為 vlaue,將其新增到 extensionClasses 集合中並返回

獲取自適應擴充套件類

getAdaptiveExtensionClass()

private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
複製程式碼

Ok,我們已經分析了 getExtensionClasses 方法,並且已經將擴充套件點實現載入到了快取中。這個方法是由 getAdaptiveExtensionClass() 方法引出來的,它看起來是像是建立自適應擴充套件類的。這裡會先判斷快取物件 cachedAdaptiveClass 是否會空,cachedAdaptiveClass 是什麼時候被初始化的呢?回顧一下之前的程式碼:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // 省略...
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            // 省略...
        }
    }
}
複製程式碼

在 loadClass() 方法中如果發現當前 clazz 包含 @Adaptive 註解,則將當前 clazz 作為快取自適應類儲存。例如在 AdaptiveExtensionFactory 類中就有這麼用,我們會將 AdaptiveExtensionFactory 類作為 ExtensionFactory 型別的自適應類快取起來。

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory
複製程式碼

我們繼續分析該方法的後部分。如果 cachedAdaptiveClass 為 null,則會呼叫 createAdaptiveExtensionClass() 方法動態生成一個自適應擴充套件類。

private Class<?> createAdaptiveExtensionClass() {
    String code = createAdaptiveExtensionClassCode();
    System.out.println(code);
    ClassLoader classLoader = findClassLoader();
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}
複製程式碼

這一段程式碼在本次分享中不打算重點敘述,可以簡單的理解為 dubbo 幫我生成了一個自適應類。我摘取了生成的一段程式碼,如下所示:

package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class ProxyFactory$Adaptive implements org.apache.dubbo.rpc.ProxyFactory {
    private static final org.apache.dubbo.common.logger.Logger logger = org.apache.dubbo.common.logger.LoggerFactory.getLogger(ExtensionLoader.class);
    private java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);

    public org.apache.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, org.apache.dubbo.common.URL arg2) throws org.apache.dubbo.rpc.RpcException {
        if (arg2 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg2;
        String extName = url.getParameter("proxy", "javassist");
        if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        org.apache.dubbo.rpc.ProxyFactory extension = null;
        try {
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        }catch(Exception e){
            if (count.incrementAndGet() == 1) {
                logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.", e);
            }
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension("javassist");
        }
        return extension.getInvoker(arg0, arg1, arg2);
    }
    public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0, boolean arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy", "javassist");
        if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        org.apache.dubbo.rpc.ProxyFactory extension = null;
        try {
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        }catch(Exception e){
            if (count.incrementAndGet() == 1) {
                logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.", e);
            }
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension("javassist");
        }
        return extension.getProxy(arg0, arg1);
    }
    public java.lang.Object getProxy(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = url.getParameter("proxy", "javassist");
        if(extName == null) throw new IllegalStateException("Fail to get extension(org.apache.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])");
        org.apache.dubbo.rpc.ProxyFactory extension = null;
        try {
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
        }catch(Exception e){
            if (count.incrementAndGet() == 1) {
                logger.warn("Failed to find extension named " + extName + " for type org.apache.dubbo.rpc.ProxyFactory, will use default extension javassist instead.", e);
            }
            extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension("javassist");
        }
        return extension.getProxy(arg0);
    }
}
複製程式碼
extension = (org.apache.dubbo.rpc.ProxyFactory)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.ProxyFactory.class).getExtension(extName);
複製程式碼

這一段程式碼實際上才是自適應適配類的精髓,看看 extName 是怎麼來的?

String extName = url.getParameter("proxy", "javassist");
複製程式碼

extName 又是從 url 中取得的,實際上 url 對於 Dubbo 來說是一種非常重要的上下文傳輸載體,在後續系列文章中大家會逐步感受到。

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();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
複製程式碼

上面的邏輯比較簡單,這裡也不贅述了,直接看 createExtension() 方法。

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()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                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);
    }
}
複製程式碼

getExtensionClasses() 方法在前文已經分析過了,但是需要注意的是:getExtensionClasses 返回給我們的不過是使用 Class.forName() 載入過的類而已,充其量執行了裡面的靜態程式碼段,而並非得到了真正的例項。真正的例項物件仍需要呼叫 class.newInstance() 方法才能獲取。
瞭解了這些之後我們繼續看,我們通過 getExtensionClasses() 嘗試獲取系統已經載入的 class 物件,通過 class 物件再去擴充套件例項快取中取。如果擴充套件例項為 null,呼叫 newInstance() 方法初始化例項,並放到 EXTENSION_INSTANCES 快取中。之後再呼叫 injectExtension() 方法進行依賴注入。最後一段涉及到包裝類的用法,下一個章節進行介紹。

擴充套件類的包裝

在 createExtension() 方法中有如下一段程式碼:

private T createExtension(String name) {
    // ···省略···
    Set<Class<?>> wrapperClasses = cachedWrapperClasses;
    if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
        for (Class<?> wrapperClass : wrapperClasses) {
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
        }
    }
    return instance;
    // ···省略···
}
複製程式碼

還記得 wrapperClasses 在什麼地方被初始化的嗎?在前文中的 loadClass() 方法中我們已經有介紹過。再回顧一下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // ···省略···
    if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    }
    // ···省略···
}

private boolean isWrapperClass(Class<?> clazz) {
    try {
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}
複製程式碼

在看這個方法前我們先了解下 Dubbo 中 wrapper 類的定義。 舉個例子:

class A {
    private A a;
    public A(A a){
        this.a = a;
    }
}
複製程式碼

我們可以看到 A 類有一個以 A 為引數的構造方法,我們稱它為複製構造方法。有這樣構造方法的類在 Dubbo 中我們稱它為 Wrapper 類。 繼續看 isWrapperClass() 方法,這個方法比較簡單,嘗試獲取 clazz 中以 type 為引數的構造方法,如果可以獲取到,則認為 clazz 則是當前 type 類的包裝類。再結合上面的程式碼,我們會發現在載入擴充套件點時,我們將對應 type 的包裝類快取起來。

private T createExtension(String name) {
    // ···省略···
    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()) {
        for (Class<?> wrapperClass : wrapperClasses) {
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
        }
    }
    return instance;
    // ···省略···
}
複製程式碼

為了更好的理解這段程式碼,我們假設當前 type 值為 Protocol.class ,我們可以在 org.apache.dubbo.rpc.Protocol 檔案中找到 Protocol 介面的包裝類 ProtocolFilterWrapper 和 ProtocolListenerWrapper,他們會依次被新增到 cachedWrapperClasses 集合中。依次遍歷 cachedWrapperClasses 集合,比如第一次取到的是 ProtocolFilterWrapper 類,則會以呼叫 ProtocolFilterWrapper 的複製構造方法將 instance 包裝起來。建立完 ProtocolFilterWrapper 物件例項後,呼叫 injectExtension() 進行依賴注入。此時 instance 已經為 ProtocolFilterWrapper 的例項,繼續迴圈,會將 ProtocolFilterWrapper 類包裝在 ProtocolListenerWrapper 類中。因此我們最後返回的是一個 ProtocolListenerWrapper 例項。最後呼叫時,仍會通過一層一層的呼叫,最後呼叫原始 instance 的方法。 這裡的包裝類有點類似 AOP 思想,我們可以通過一層一層的包裝,在呼叫擴充套件實現之前新增一些日誌列印、監控等自定義的操作。

Dubbo 中的 IOC 機制

上文中我們已經討論過 Dubbo 中利用反射機制實現一個類 IOC 功能。在這一章節中,我們再回顧一下 injectExtension() 方法,仔細的來看看 Dubbo 中 IOC 功能的實現。

createExtension()
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));

private T injectExtension(T instance) {
    // ···
    Class<?> pt = method.getParameterTypes()[0];
    try {
        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
        Object object = objectFactory.getExtension(pt, property);
        if (object != null) {
            method.invoke(instance, object);
        }
    }
    // ···
}

public class StubProxyFactoryWrapper implements ProxyFactory {
    // ...
    private Protocol protocol;
    public void setProtocol(Protocol protocol) {
        this.protocol = protocol;
    }
    //...
}
複製程式碼

在上一章節中我們已經講過 wrapper 類,在這裡我們舉個例子說明一下。比如我們當前的 wrapperClass 類為 StubProxyFactoryWrapper,那麼程式碼執行邏輯大致如下所示:

  1. 建立 StubProxyFactoryWrapper 例項;
  2. 獲取流程1建立的例項作為 injectExtension() 的引數,執行;
  3. injectExtension() 方法迴圈遍歷到 StubProxyFactoryWrapper 的 setProtocol()方法(此時 pt=Protocol.class,property=protocol),執行 objectFactory.getExtension(pt,property) 方法。objectFactory 在 ExtensionLoader 的構造方法中被初始化,在這裡獲取到自適應擴充套件類為 AdaptiveExtensionFactory。
  4. 執行 AdaptiveExtensionFactory.getExtension()。AdaptiveExtensionFactory 類中有一個集合變數 factories。factories 在 AdaptiveExtensionFactory 的構造方法中被初始化,包含了兩個工廠類:SpiExtensionFactory、SpringExtensionFactory。執行 AdaptiveExtensionFactory 類的 getExtension() 方法會依次呼叫 SpiExtensionFactory 和 SpringExtensionFactory 類的 getExtension() 方法。
  5. 執行 SpiExtensionFactory 的 getExtension() 方法。上面有說到此時的 type=Procotol.class,property=protocol,從下面的程式碼我們可以發現 Protocol 是一個介面類,同時標註了 @SPI 註解,此時會獲取 Protocol 型別的 ExtensionLoader 物件,最後又去呼叫 loader 的 getAdaptiveExtension() 方法。最終獲取到的自適應類為 Protocol$Adaptive 動態類。
  6. objectFactory.getExtension(pt, property); 最後得到的類為 Protocol$Adaptive 類,最後利用反射機制將其注入到 StubProxyFactoryWrapper 例項中。
@SPI("dubbo")
public interface Protocol {
}
public class SpiExtensionFactory implements ExtensionFactory {
    @Override
    public <T> T getExtension(Class<T> type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }
}
複製程式碼

END

在最後,我們再回顧下開頭關於 Dubbo SPI 基於 JAVA SPI 改進的那段話:

Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。 JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。 如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當用戶執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點

總結如下:

  1. Dubbo SPI 在載入擴充套件點,會以 key-value 的形式將擴充套件類儲存在快取中,但此時的擴充套件類只是呼叫 Class.forName() 載入的類,並沒有例項化。擴充套件類會在呼叫 getExtension() 方法時被例項化。
  2. Dubbo 通過工廠模式和反射機制實現了依賴注入功能。
  3. Dubbo 中通過包裝類實現了 AOP 機制,方便我們新增監控和列印日誌。