Dubbo2.7的Dubbo SPI實現原理細節

朱季謙發表於2023-02-05

總結/朱季謙

本文主要記錄我對Dubbo SPI實現原理的理解,至於什麼是SPI,我這裡就不像其他博文一樣詳細地從概念再到Java SPI細細分析了,直接開門見山來分享我對Dubbo SPI的見解。

Dubbo SPI的機制比較類似Spring IOC的getBean()載入,當傳入一個存在的beanName,就可以返回該beanName對應的物件。同理,在Dubbo SPI中,我們同樣傳入一個存在的name,Dubbo框架會自動返回該key對應的物件。不難猜測,Dubbo SPI與Spring IOC在底層應該都有一個大致相似的邏輯,簡單的說,就是兩者都可透過beanName或key值,到框架裡的某個map快取當中,找到對應的class類名,然後對該class類名進行反射生成物件,初始化完成該物件,最後返回一個完整的物件。然而,在這個過程當中,Spirng相對來說會更復雜,它裡面還有一堆後置處理器......

簡單舉一個例子,大概解釋一下Dubbo SPI實現原理,然後再進一步分析原始碼。

首先,我在org.apache.dubbo.test目錄下,定義一個@SPI註解到介面:

package org.apache.dubbo.test;

import org.apache.dubbo.common.extension.SPI;

@SPI("dog")
public interface Animal {
    void haveBehavior();
}

然後,在同一個目錄下,建立兩個實現該介面的類,分別為Dog,Cat。

Dog——

package org.apache.dubbo.test;

public class Dog implements Animal {

    @Override
    public void haveBehavior() {
        System.out.println("狗會叫");
    }
}

Cat——

package org.apache.dubbo.test;

public class Cat implements Animal {
    @Override
    public void haveBehavior() {
        System.out.println("貓會抓老鼠");
    }
}

注意看,Animal介面的類名為org.apache.dubbo.test.Animal,接下來,我們在resource目錄的/META_INF/dubbo需新建一個對應到該介面名的File檔案,檔名與Animal介面的類名一致:org.apache.dubbo.test.Animal。之所以兩者名字要一致,是因為這樣只需拿到Animal介面的類名,到resource目錄的/META_INF/dubbo,就可以透過該類名,定位到與Animal介面相對應的檔案。

在Dubbo中,檔名org.apache.dubbo.test.Animal的檔案裡,其實存了類似Spring bean那種的資料,即id對應bean class的形式——

cat=org.apache.dubbo.test.Cat
dog=org.apache.dubbo.test.Dog

這兩行資料,分別是Animal介面的實現類Cat和Dog的class全限名。

整個的目錄結構是這樣的——
image

最後寫一個測試類DubboSPITestTest演示一下效果——

package org.apache.dubbo.test;

import org.apache.dubbo.common.extension.ExtensionLoader;
import org.junit.jupiter.api.Test;

class DubboSPITestTest {

    @Test
    public void test(){

        Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog");
        dog.haveBehavior();

        Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
        cat.haveBehavior();

    }
}

執行結果如下——

image

先簡單捋一下這個思路是怎麼實現的,ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog")這行程式碼內部,會根據Animal介面完整名org.apache.dubbo.test.Animal找到某個指定目錄下的同名File檔案org.apache.dubbo.test.Animal,然後按行迴圈解析檔案裡的內容,以key-value形式載入到某個map快取裡。

類似這樣操作(當然,原始碼裡會更復雜些)——

Map<String,String> map = new HashMap<>();
map.put("cat","org.apache.dubbo.test.Cat");
map.put("dog","org.apache.dubbo.test.Dog");

當然,真實原始碼裡,value存的是已根據類名得到的Class,但其實無論是類名還是Class,最後都是可以反射生成物件的,

這時候,就可以根據執行程式碼去動態獲取到該介面對應的實現類了。例如,需要使用的是org.apache.dubbo.test.Cat這個實現類,那麼在呼叫getExtension("cat")方法中,我們傳入的引數是"cat",就會從剛剛解析檔案快取的map中,根據map.get("cat")拿到對應org.apache.dubbo.test.Cat。既然能拿到類名了,不就可以透過反射的方式生成該類的物件了嗎?當然,生成的物件裡可能還有屬性需要做注入操作,這就是Dubbo IOC的功能,這塊會在原始碼分析裡進一步說明。當物件完成初始化後,就會返回生成的物件指向其介面引用Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog")。

整個過程,就是可根據程式碼去動態獲取到某個介面的實現類,方便靈活呼叫的同時,實現了介面和實現類的解耦。

Dubbo SPI是在Java SPI基礎上做了擴充,Java SPI中與介面同名的檔案裡,並不是key- value的形式,純粹是按行來直接存放各實現類的類名,例如這樣子——

org.apache.dubbo.test.Cat
org.apache.dubbo.test.Dog

這就意味著,Java SPI在實現過程中,透過介面名定位讀取到resource中介面同名檔案時,是無法做到去選擇性地根據某個key值來選擇某個介面的實現類,它只能全部讀取,再全部迴圈獲取到對應介面實現類呼叫相應方法。這就意味著,可能存在一些並非需要呼叫的實現類,也會被載入並生成物件一同返回來,無法做到按需獲取。

因此,Dubbo在原有基礎上,則補充了Java SPI無法按需透過某個key值去呼叫指定的介面實現類,例如上面提到的,Dubbo SPI可透過cat這個key,去按需返回對應的org.apache.dubbo.test.Cat類的實現物件。

下面就來分析一下具體實現的原理細節,以下程式碼做案例。

Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
cat.haveBehavior();

先來分析ExtensionLoader.getExtensionLoader(Animal.class)方法——

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 an interface!");
    }
  
    //判斷是否具有@SPI註解
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
                ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }

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

在這個方法裡,若傳進來的Class引數為空或者非介面或者沒有@SPI註解,都會丟擲一個IllegalArgumentException異常,說明傳進來的Class必須需要滿足非空,為介面,同時具有@SPI註解修飾,才能正常往下執行。我們在這裡傳進來的是Animal.class,它滿足了以上三個條件。

@SPI("cat")
public interface Animal {
    void haveBehavior();
}

接下來,在最後這部分程式碼裡,主要是建立一個ExtensionLoader物件。

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;

最後,返回的也是建立的ExtensionLoader物件,該物件包括了兩個東西,一個是type,一個是objectFactory。這兩個東西在後面原始碼裡都會用到。
image

建立完ExtensionLoader物件後,就會開始呼叫getExtension方法——

Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");

進入到getExtension("cat")方法當中,內部會呼叫另一個過載方法getExtension(name, true)。

public T getExtension(String name) {
    return getExtension(name, true);
}

讓我們來看一下該方法內部實現——

public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    //step 1
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    
    //step 2
    //雙重檢查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                //建立擴充套件例項
                instance = createExtension(name, wrap);
                //設定例項到holeder中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

該方法主要有兩部分。

step 1,先從快取裡查詢name為“cat”的物件是否存在,即呼叫getOrCreateHolder(name),在該方法裡,會去cachedInstances快取裡查詢。cachedInstances是一個定義為ConcurrentMap<String, Holder>的map快取。若cachedInstances.get(name)返回null的話,說明快取裡還沒有name對應的物件資料,那麼就會建立一個key值為name,value值為new Holder<>()的鍵值對快取。

private Holder<Object> getOrCreateHolder(String name) {
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<>());
        holder = cachedInstances.get(name);
    }
    return holder;
}

想必你一定會有疑惑,為什麼這裡要建立一個new Holder<>()物件呢?

進到Holder類裡,就會發現,其內部用private修飾封裝一個泛型變數value,這就意味著,外部類是無法修改該value值,能起到一個封裝保護的作用。我們正在透過name='cat'去得到一個org.apache.dubbo.test.Cat實現類物件,該物件若能正常生成,最後就會封裝到一個Holder物件裡,再將Holder物件存放到cachedInstances快取裡。

public class Holder<T> {
    private volatile T value;
    public void set(T value) {
        this.value = value;
    }
    public T get() {
        return value;
    }
}

因此,就會有該從快取裡獲取Holder的操作——

//根據name為“cat”去快取裡查詢封裝了org.apache.dubbo.test.Cat物件的Holder物件。
final Holder<Object> holder = getOrCreateHolder(name);
//若能查詢到,就從Holder物件取出內部封裝的物件
Object instance = holder.get();

若holder.get()得到的物件為null,說明還沒有生成該“cat”對應的org.apache.dubbo.test.Cat類物件。

那麼,就會繼續往下執行——

//雙重檢查
if (instance == null) {
    synchronized (holder) {
        instance = holder.get();
        if (instance == null) {
            //建立擴充套件例項
            instance = createExtension(name, wrap);
            //設定例項到holeder中
            holder.set(instance);
        }
    }
}

這裡用到了一個雙重檢查的操作,避免在多執行緒情況裡出現某一個執行緒建立了一半,另一個執行緒又開始建立同樣物件,就會出現問題。

這行instance = createExtension(name, wrap)程式碼,主要實現的功能,就是得到“cat”對應的org.apache.dubbo.test.Cat類物件,然後將返回的物件透過holder.set(instance)封裝在Holder物件裡。

private T createExtension(String name, boolean wrap) {
    // step 1從配置檔案中載入所有的擴充套件類,可得到"配置項名稱"到"配置類"的對映關係表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            //step 2  將得到的clazz透過反射建立例項物件。
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        //step 3  對例項物件的屬性做依賴注入,即Dubbo IOC邏輯。
        injectExtension(instance);


        if (wrap) {
            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        //將當前instance作為引數傳給Wrapper的構造方法,並透過反射建立Wrapper例項
                        //然後向Wrapper例項注入依賴,最後將Wrapper例項再次賦值給instance變數
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                    }
                }
            }
        }

        initExtension(instance);
        //step 4 返回初始化完成的物件
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

createExtension(String name, boolean wrap)方法裡主要實現了以下

step 1 從配置檔案中載入所有的擴充套件類,可得到"配置項名稱"到"配置類"的對映關係表。

step 2 將得到的clazz透過反射建立例項物件。

step 3 對例項物件的屬性做依賴注入,即Dubbo IOC邏輯。

step 4 返回初始化完成的物件

一、先來看第一步的程式碼分析——

// step 1從配置檔案中載入所有的擴充套件類,可得到"配置項名稱"到"配置類"的對映關係表
Class<?> clazz = getExtensionClasses().get(name);

其中getExtensionClasses()方法是獲取返回一個解析完介面對應Resource裡檔案的Map<String, Class>快取,程式碼最後部分get(name)在這個案例裡,就是根據“cat”獲得“org.apache.dubbo.test.Cat”的Class。方法內部cachedClasses.get()返回的這個Map> classes正是存放了介面對應Resource檔案裡key- value資料,即car=org.apache.dubbo.test.Cat和dog=org.apache.dubbo.test.Dog這類。

private Map<String, Class<?>> getExtensionClasses() {
    //從快取中獲取已載入的擴充類:car=org.apache.dubbo.test.Cat
    Map<String, Class<?>> classes = cachedClasses.get();
    //雙重檢查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

當然,首次呼叫cachedClasses.get()返回值classes肯定為null。這裡在classes==null時,同樣使用了一個雙重檢查的操作,最後會去呼叫loadExtensionClasses()方法,該方法主要做的一件事,就是去讀取到Resource中介面對應的檔案進行解析,然後將解析的資料以key-value快取到Map<String, Class<?>>裡,最後透過cachedClasses.set(classes)存入到cachedClasses裡,這裡的cachedClasses同樣是一個final定義的Holder物件,作用與前文提到的一致,都是封裝在內部以private修飾,防止被外部類破壞。

主要看下loadExtensionClasses()內部邏輯——

private Map<String, Class<?>> loadExtensionClasses() {
    //step 1 對SPI註解進行解析,獲取SPI預設對value
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();

    /**
     * step 2 strategies包含以下四種策略,代表查詢四種不同目錄下的檔案:
     *
     *      DubboInternalLoadingStrategy 表示目錄"META-INF/dubbo/internal/"
     *      DubboExternalLoadingStrategy 表示目錄""META-INF/dubbo/external/""
     *      DubboLoadingStrategy 表示目錄"META-INF/dubbo/"
     *      ServicesLoadingStrategy 表示目錄"META-INF/services/"
     */
    for (LoadingStrategy strategy : strategies) {
        //載入指定資料夾下對配置檔案,找到SPI預設對value的class
        //apache
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        //alibaba
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

首先,執行cacheDefaultExtensionName()方法,該方法是對介面修飾的@SPI進行解析,獲取註解裡的value值。例如,在該例子裡,Animal的註解@SPI("cat"),那麼,透過cacheDefaultExtensionName()方法,即能獲取到註解@SPI裡的預設值“cat”。之所以獲取該註解的值,是用來當做預設值,即如果沒有傳入指定需要獲取的name,那麼就返回cat對應的類物件。

image

接著,就是遍歷四種不同目錄,查詢是否有與介面Animal對應的檔案。這裡的strategies是一個陣列,裡面包含四種物件,每個物件代表查詢某個目錄,包括"META-INF/dubbo/internal/"、"META-INF/dubbo/external/"、"META-INF/dubbo/"、"META-INF/services/",表示分別到這四種目錄去檢視是否有滿足的檔案。

for迴圈裡呼叫了兩次loadDirectory方法,群分就是一個是查詢apache版本的,一個是查詢alibaba版本的,兩方法底層其實都是一樣,只需要關注其中一個實現即可。

   /**
     * step 2 strategies包含以下四種策略,代表查詢四種不同目錄下的檔案:
     *
     *      DubboInternalLoadingStrategy 表示目錄"META-INF/dubbo/internal/"
     *      DubboExternalLoadingStrategy 表示目錄"META-INF/dubbo/external/"
     *      DubboLoadingStrategy 表示目錄"META-INF/dubbo/"
     *      ServicesLoadingStrategy 表示目錄"META-INF/services/"
     */
    for (LoadingStrategy strategy : strategies) {
        //載入指定資料夾下對配置檔案,找到SPI預設對value的class
        //apache
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        //alibaba
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

在loadDirectory方法裡,就是定位到介面對應的File檔案,獲取檔案的路徑,然後呼叫loadResource方法對檔案進行解析——

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                           boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
    //資料夾路徑+type 全限定名:META-INF/dubbo/internal/org.apache.dubbo.test.Animal
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls = null;
        ClassLoader classLoader = findClassLoader();

        // try to load from ExtensionLoader's ClassLoader first
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader.getResources(fileName);
            }
        }

        if (urls == null || !urls.hasMoreElements()) {
            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, overridden, excludedPackages);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

loadResource方法主要是讀取File檔案資源,然後迴圈遍歷檔案裡的每一行記錄,跳過開頭為#的註釋記錄,對cat=org.apache.dubbo.test.Cat形式的行記錄進行切割。透過這行程式碼int i = line.indexOf('=')定位到等於號=的位置,然後以name = line.substring(0, i).trim()來擷取等於號前面的字串作為key, 以 line = line.substring(i + 1).trim()擷取等於號=後面的字串作為value,形成key-value鍵值對形式資料,進一步傳到 loadClass方法進行相應快取。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            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) {
                            //以等於號 = 為界,讀取key健 value值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
                            //載入類,透過loadClass對類進行快取
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
                        }
                    } 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);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

以cat=org.apache.dubbo.test.Cat資料為例子,debug可以看到,最後解析得到的為——

image

最後,到loadClass看一下是怎麼對從檔案裡解析出來的key-value資料進行快取,注意一點是,在執行該方法時,已將上文拿到的line="org.apache.dubbo.test.Cat"透過Class.forName(line, true, classLoader)生成了對應的Class。

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    //檢測目標類上是否有Adaptive註解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        //設定cachedadaptiveClass快取
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        //程式進入此分支,表明class是一個普通的擴充類
        //檢測class是否有預設的構造方法,如果沒有,則丟擲異常
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            //如果name為空,則嘗試從Extension註解中獲取name,或使用小寫的類名作為name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }
        //names = ["cat"]
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                //儲存Class到名稱的對映關係
                cacheName(clazz, n);
                //儲存名稱到Class的對映關係
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

這裡只需要關注最後的saveInExtensionClass方法,可以看到,最後將從檔案裡解析出來的“cat”-->org.apache.dubbo.test.Cat存入到Map<String, Class<?>> extensionClasses快取當中。

image

這個Map<String, Class<?>> extensionClasses快取是在loadExtensionClasses()方法裡建立的,該loadExtensionClasses方法最後會將extensionClasses進行返回。

private Map<String, Class<?>> loadExtensionClasses() {
    ......
    //建立用來快取儲存解析檔案裡key-value資料
    Map<String, Class<?>> extensionClasses = new HashMap<>();

    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        ......

    return extensionClasses;
}

到這一步,就完成了Animal介面對應的resource/META-INF/dubbo/org.apache.dubbo.test.Animal檔案的解析,解析出來的資料存放到了extensionClasses這個Map快取裡。

image

回顧先前的方法呼叫,可以看到,最終得到extensionClasses的map快取,會返回到getExtensionClasses()方法,因此,在createExtension呼叫getExtensionClasses().get(name),就相當於是呼叫extensionClasses.get(name)。因為傳到方法裡的引數name="cat",故而返回的Class即org.apache.dubbo.test.Cat。

image

接著往下執行,到程式碼EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance())就是透過clazz.newInstance()反射建立了一個暫時還是空屬性的物件,同時快取到EXTENSION_INSTANCES快取裡,這是一個ConcurrentMap<Class<?>, Object>快取,避免反覆進行反射建立物件。
image

例項化完成org.apache.dubbo.test.Cat物件的建立,接下來就是透過injectExtension(instance)對物件進行依賴注入了。主要功能就類似Spring IOC裡bean存在@Resource或者@Autowired註解的屬性時,該bean在例項化建立完物件後,需要對屬性進行補充,即將@Resource或者@Autowired註解的屬性透過反射的方式,指向另外的bean物件。在Dubbo IOC裡,同樣是類似的實現。首先,會過濾掉那些非setXxx()的方法,只對setXxx()方法處理。這種處理方式,就是擷取set後面的字元,例如,存在這樣一個setHitInformService (HitInformService hitInformService)方法,那麼就會擷取set後面的字元,並且對擷取後的第一個字元做小寫處理,得到“hitInformService”。注意一點是,同時,會獲取引數裡的型別HitInformService,如果型別為陣列、String、Boolean、Character、Number、Date其中一個,則不會對其進行注入操作。反之,就會繼續往下執行。

/**
 * Dubbo IOC目前僅支援setter方式注入
 * @param instance
 * @return
 */
private T injectExtension(T instance) {

    if (objectFactory == null) {
        return instance;
    }

    try {
        //遍歷目標類的所有方法
        for (Method method : instance.getClass().getMethods()) {
            //檢測方法是否以set開頭,且方法僅有一個引數,且方法訪問級別為public
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Check {@link DisableInject} to see if we need auto injection for this property
             */
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            //獲取setter方法引數型別
            Class<?> pt = method.getParameterTypes()[0];
            //判斷該物件是否為陣列、String、Boolean、Character、Number、Date型別,若是,則跳出本次迴圈,繼續下一次迴圈
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                //獲取屬性名,比如 setName方法對應屬性名name
                String property = getSetterProperty(method);
                /**
                 * objectFactory 變數的型別為 AdaptiveExtensionFactory,
                 *      AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 列表,用於儲存其他型別的 ExtensionFactory。
                 *
                 * Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。
                 *      前者用於建立自適應的擴充,後者是用於從 Spring 的 IOC 容器中獲取所需的擴充。
                 *
                 */
                //從ObjectFactory中獲取依賴物件
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                    //透過反射呼叫setter方法依賴
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type.getName() + ": " + e.getMessage(), e);
            }

        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

執行 Object object = objectFactory.getExtension(pt, property)到行程式碼,就是去獲取HitInformService hitInformService引用對應的物件,這裡獲取有兩種方式,一種是透過SpringExtensionFactory去透過getBean(name)走Spring載入bean的方式獲取物件,另一種是透過本文的Dubbo SPI方式,根據name去解析檔案裡對應的介面實現類Class反射生成返回。

無論是透過哪種方式,最後都需獲取返回一個物件,然後透過method.invoke(instance, object)反射去執行對應的setXxx()方法,將物件進行屬性注入到前文SPI建立的物件cat裡。

到這裡,就完成了介面Animal對應cat這個實現類的建立了,這個過程,就是Dubbo SPI的底層實現細節。最後,將得到的org.apache.dubbo.test.Cat物件向上指向其介面Animal引用,透過介面就可以呼叫該實現類重寫的haveBehavior方法了。

Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");
cat.haveBehavior();

相關文章