天天看點

Dubbo SPI 實作

引言

前文中,我們已經介紹 Dubbo SPI 和 Java SPI 的差別以及一些增強,本文我們着重介紹 Dubbo SPI 的實作方式,其他 Dubbo 相關文章均收錄于

<Dubbo系列文章>

實作原理

SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類。這樣可以在運作時,動态為接口替換實作類。正是以特性,我們可以很容易的通過 SPI 機制為我們的程式提供拓展功能。SPI 機制在第三方架構中也有所應用,比如 Dubbo 就是通過 SPI 機制加載所有的元件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的子產品。基于 SPI,我們可以很容易的對 Dubbo 進行拓展。

我們首先通過 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;
}           

上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則建立拓展對象。下面我們來看一下建立拓展對象的過程是怎樣的。

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

createExtension 方法的邏輯稍複雜一下,包含了如下的步驟:

  1. 通過 getExtensionClasses 擷取所有的拓展類
  2. 通過反射建立拓展對象
  3. 向拓展對象中注入依賴
  4. 将拓展對象包裹在相應的 Wrapper 對象中

以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實作。在接下來的章節中,将會重點分析 getExtensionClasses 方法的邏輯,以及簡單介紹 Dubbo IOC 的具體實作。

擷取所有的拓展類

我們在通過名稱擷取拓展類之前,首先需要根據配置檔案解析出拓展項名稱到拓展類的映射關系表(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;
}           

這裡也是先檢查緩存,若緩存未命中,則通過 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;
}           

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

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 等等。除此之外,該方法沒有其他什麼邏輯了。

到此,關于緩存類加載的過程就分析完了。整個過程沒什麼特别複雜的地方,大家按部就班的分析即可,不懂的地方可以調試一下。接下來,我們來聊聊 Dubbo IOC 方面的内容。

Dubbo IOC

Dubbo IOC 是通過 setter 方法注入依賴。Dubbo 首先會通過反射擷取到執行個體的所有方法,然後再周遊方法清單,檢測方法名是否具有 setter 方法特征。若有,則通過 ObjectFactory 擷取依賴對象,最後通過反射調用 setter 方法将依賴設定到目标對象中。整個過程對應的代碼如下:

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            // 周遊目标類的所有方法
            for (Method method : instance.getClass().getMethods()) {
                // 檢測方法是否以 set 開頭,且方法僅有一個參數,且方法通路級别為 public
                if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                    // 擷取 setter 方法參數類型
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        // 擷取屬性名,比如 setName 方法對應屬性名 name
                        String property = method.getName().length() > 3 ?
                            method.getName().substring(3, 4).toLowerCase() +
                                method.getName().substring(4) : "";
                        // 從 ObjectFactory 中擷取依賴對象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 通過反射調用 setter 方法設定依賴
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("fail to inject via method...");
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}           

在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部維護了一個 ExtensionFactory 清單,用于存儲其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于建立自适應的拓展,後者是用于從 Spring 的 IOC 容器中擷取所需的拓展。

Dubbo IOC 目前僅支援 setter 方式注入,總的來說,邏輯比較簡單易懂。

Dubbo 自适應機制

在 Dubbo 中,很多拓展都是通過 SPI 機制進行加載的,比如 Protocol、Cluster、LoadBalance 等。有時,有些拓展并不想在架構啟動階段被加載,而是希望在拓展方法被調用時,根據運作時參數進行加載。這聽起來有些沖突。拓展未被加載,那麼拓展方法就無法被調用(靜态方法除外)。拓展方法未被調用,拓展就無法被加載。對于這個沖突的問題,Dubbo 通過自适應拓展機制很好的解決了。自适應拓展機制的實作邏輯比較複雜,首先 Dubbo 會為拓展接口生成具有代理功能的代碼。然後通過 javassist 或 jdk 編譯這段代碼,得到 Class 類。最後再通過反射建立代理類,整個過程比較複雜。為了讓大家對自适應拓展有一個感性的認識,下面我們通過一個示例進行示範。這是一個與汽車相關的例子,我們有一個車輪制造廠接口 WheelMaker:

在對自适應拓展生成過程進行深入分析之前,我們先來看一下與自适應拓展息息相關的一個注解,即 Adaptive 注解。該注解的定義如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}           

從上面的代碼中可知,Adaptive 可注解在類或方法上。當 Adaptive 注解在類上時,Dubbo 不會為該類生成代理類。注解在方法(接口方法)上時,Dubbo 則會為該方法生成代理邏輯。Adaptive 注解在類上的情況很少,在 Dubbo 中,僅有兩個類被 Adaptive 注解了,分别是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此種情況,表示拓展的加載邏輯由人工編碼完成。更多時候,Adaptive 是注解在接口方法上的,表示拓展的加載邏輯需由架構自動生成。Adaptive 注解的地方不同,相應的處理邏輯也是不同的。注解在類上時,處理邏輯比較簡單,會直接作為自适應類。注解在接口方法上時,處理邏輯較為複雜,接下來将會重點分析此塊邏輯。

擷取自适應拓展

getAdaptiveExtension 方法是擷取自适應拓展的入口方法,是以下面我們從這個方法進行分析。相關代碼如下:

public T getAdaptiveExtension() {
    // 從緩存中擷取自适應拓展
    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) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("fail to create adaptive instance: ...");
                    }
                }
            }
        } else {
            throw new IllegalStateException("fail to create adaptive instance:  ...");
        }
    }

    return (T) instance;
}           

getAdaptiveExtension 方法首先會檢查緩存,緩存未命中,則調用 createAdaptiveExtension 方法建立自适應拓展。下面,我們看一下 createAdaptiveExtension 方法的代碼。

private T createAdaptiveExtension() {
    try {
        // 擷取自适應拓展類,并通過反射執行個體化
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can not create adaptive extension ...");
    }
}           

createAdaptiveExtension 方法的代碼比較少,但卻包含了三個邏輯,分别如下:

  1. 調用 getAdaptiveExtensionClass 方法擷取自适應拓展 Class 對象
  2. 通過反射進行執行個體化
  3. 調用 injectExtension 方法向拓展執行個體中注入依賴

前兩個邏輯比較好了解,第三個邏輯用于向自适應拓展對象中注入依賴。這個邏輯看似多餘,但有存在的必要,這裡簡單說明一下。前面說過,Dubbo 中有兩種類型的自适應拓展,一種是手工編碼的,一種是自動生成的。手工編碼的自适應拓展中可能存在着一些依賴,而自動生成的 Adaptive 拓展則不會依賴其他類。這裡調用 injectExtension 方法的目的是為手工編碼的自适應拓展注入依賴,這一點需要大家注意一下。關于 injectExtension 方法,前文已經分析過了,這裡不再贅述。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。

private Class<?> getAdaptiveExtensionClass() {
    // 通過 SPI 擷取所有的拓展類
    getExtensionClasses();
    // 檢查緩存,若緩存不為空,則直接傳回緩存
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 建立自适應拓展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}           

getAdaptiveExtensionClass 方法同樣包含了三個邏輯,如下:

  1. 調用 getExtensionClasses 擷取所有的拓展類
  2. 檢查緩存,若緩存不為空,則傳回緩存
  3. 若緩存為空,則調用 createAdaptiveExtensionClass 建立自适應拓展類

這三個邏輯看起來平淡無奇,似乎沒有多講的必要。但是這些平淡無奇的代碼中隐藏了着一些細節,需要說明一下。首先從第一個邏輯說起,getExtensionClasses 這個方法用于擷取某個接口的所有實作類。比如該方法可以擷取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等實作類。在擷取實作類的過程中,如果某個某個實作類被 Adaptive 注解修飾了,那麼該類就會被指派給 cachedAdaptiveClass 變量。此時,上面步驟中的第二步條件成立(緩存不為空),直接傳回 cachedAdaptiveClass 即可。如果所有的實作類均未被 Adaptive 注解修飾,那麼執行第三步邏輯,建立自适應拓展類。相關代碼如下:

private Class<?> createAdaptiveExtensionClass() {
    // 建構自适應拓展代碼
    String code = createAdaptiveExtensionClassCode();
    ClassLoader classLoader = findClassLoader();
    // 擷取編譯器實作類
    com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // 編譯代碼,生成 Class
    return compiler.compile(code, classLoader);
}           

createAdaptiveExtensionClass 方法用于生成自适應拓展類,該方法首先會生成自适應拓展類的源碼,然後通過 Compiler 執行個體(Dubbo 預設使用 javassist 作為編譯器)編譯源碼,得到代理類 Class 執行個體。接下來,我們把重點放在代理類代碼生成的邏輯上,其他邏輯大家自行分析。

拓展類代碼生成

在生成代理類源碼之前,createAdaptiveExtensionClassCode 方法首先會通過反射檢測接口方法是否包含 Adaptive 注解。對于要生成自适應拓展的接口,Dubbo 要求該接口至少有一個方法被 Adaptive 注解修飾。若不滿足此條件,就會抛出運作時異常。相關代碼如下:

// 通過反射擷取所有的方法
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
// 周遊方法清單
for (Method m : methods) {
    // 檢測方法上是否有 Adaptive 注解
    if (m.isAnnotationPresent(Adaptive.class)) {
        hasAdaptiveAnnotation = true;
        break;
    }
}

if (!hasAdaptiveAnnotation)
    // 若所有的方法上均無 Adaptive 注解,則抛出異常
    throw new IllegalStateException("No adaptive method on extension ...");           

通過 Adaptive 注解檢測後,即可開始生成代碼。代碼生成的順序與 Java 檔案内容順序一緻,首先會生成 package 語句,然後生成 import 語句,緊接着生成類名等代碼。整個邏輯如下:

// 生成 package 代碼:package + type 所在包
codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
// 生成 import 代碼:import + ExtensionLoader 全限定名
codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
// 生成類代碼:public class + type簡單名稱 + $Adaptive + implements + type全限定名 + {
codeBuilder.append("\npublic class ")
    .append(type.getSimpleName())
    .append("$Adaptive")
    .append(" implements ")
    .append(type.getCanonicalName())
    .append(" {");

// ${生成方法}

codeBuilder.append("\n}");           

這裡使用 ${...} 占位符代表其他代碼的生成邏輯,該部分邏輯将在随後進行分析。上面代碼不是很難了解,下面直接通過一個例子展示該段代碼所生成的内容。以 Dubbo 的 Protocol 接口為例,生成的代碼如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    // 省略方法代碼
}           

一個方法可以被 Adaptive 注解修飾,也可以不被修飾。這裡将未被 Adaptive 注解修飾的方法稱為“無 Adaptive 注解方法”,下面我們先來看看此種方法的代碼生成邏輯是怎樣的。

對于接口方法,我們可以按照需求标注 Adaptive 注解。以 Protocol 接口為例,該接口的 destroy 和 getDefaultPort 未标注 Adaptive 注解,其他方法均标注了 Adaptive 注解。Dubbo 不會為沒有标注 Adaptive 注解的方法生成代理邏輯,對于該種類型的方法,僅會生成一句抛出異常的代碼。生成邏輯如下:

for (Method method : methods) {

    // 省略無關邏輯

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    // 如果方法上無 Adaptive 注解,則生成 throw new UnsupportedOperationException(...) 代碼
    if (adaptiveAnnotation == null) {
        // 生成的代碼格式如下:
        // throw new UnsupportedOperationException(
        //     "method " + 方法簽名 + of interface + 全限定接口名 + is not adaptive method!”)
        code.append("throw new UnsupportedOperationException(\"method ")
            .append(method.toString()).append(" of interface ")
            .append(type.getName()).append(" is not adaptive method!\");");
    } else {
        // 省略無關邏輯
    }

    // 省略無關邏輯
}
           

以 Protocol 接口的 destroy 方法為例,上面代碼生成的内容如下:

throw new UnsupportedOperationException(
            "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");           

前面說過方法代理邏輯會從 URL 中提取目标拓展的名稱,是以代碼生成邏輯的一個重要的任務是從方法的參數清單或者其他參數中擷取 URL 資料。舉例說明一下,我們要為 Protocol 接口的 refer 和 export 方法生成代理邏輯。在運作時,通過反射得到的方法定義大緻如下:

Invoker refer(Class<T> arg0, URL arg1) throws RpcException;
Exporter export(Invoker<T> arg0) throws RpcException;           

對于 refer 方法,通過周遊 refer 的參數清單即可擷取 URL 資料,這個還比較簡單。對于 export 方法,擷取 URL 資料則要麻煩一些。export 參數清單中沒有 URL 參數,是以需要從 Invoker 參數中擷取 URL 資料。擷取方式是調用 Invoker 中可傳回 URL 的 getter 方法,比如 getUrl。如果 Invoker 中無相關 getter 方法,此時則會抛出異常。整個邏輯如下:

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // ${無 Adaptive 注解方法代碼生成邏輯}
    } else {
        int urlTypeIndex = -1;
        // 周遊參數清單,确定 URL 參數位置
        for (int i = 0; i < pts.length; ++i) {
            if (pts[i].equals(URL.class)) {
                urlTypeIndex = i;
                break;
            }
        }

        // urlTypeIndex != -1,表示參數清單中存在 URL 參數
        if (urlTypeIndex != -1) {
            // 為 URL 類型參數生成判空代碼,格式如下:
            // if (arg + urlTypeIndex == null)
            //     throw new IllegalArgumentException("url == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                     urlTypeIndex);
            code.append(s);

            // 為 URL 類型參數生成指派代碼,形如 URL url = arg1
            s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
            code.append(s);

        // 參數清單中不存在 URL 類型參數
        } else {
            String attribMethod = null;

            LBL_PTS:
            // 周遊方法的參數類型清單
            for (int i = 0; i < pts.length; ++i) {
                // 擷取某一類型參數的全部方法
                Method[] ms = pts[i].getMethods();
                // 周遊方法清單,尋找可傳回 URL 的 getter 方法
                for (Method m : ms) {
                    String name = m.getName();
                    // 1. 方法名以 get 開頭,或方法名大于3個字元
                    // 2. 方法的通路權限為 public
                    // 3. 非靜态方法
                    // 4. 方法參數數量為0
                    // 5. 方法傳回值類型為 URL
                    if ((name.startsWith("get") || name.length() > 3)
                        && Modifier.isPublic(m.getModifiers())
                        && !Modifier.isStatic(m.getModifiers())
                        && m.getParameterTypes().length == 0
                        && m.getReturnType() == URL.class) {
                        urlTypeIndex = i;
                        attribMethod = name;

                        // 結束 for (int i = 0; i < pts.length; ++i) 循環
                        break LBL_PTS;
                    }
                }
            }
            if (attribMethod == null) {
                // 如果所有參數中均不包含可傳回 URL 的 getter 方法,則抛出異常
                throw new IllegalStateException("fail to create adaptive class for interface ...");
            }

            // 為可傳回 URL 的參數生成判空代碼,格式如下:
            // if (arg + urlTypeIndex == null)
            //     throw new IllegalArgumentException("參數全限定名 + argument == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                     urlTypeIndex, pts[urlTypeIndex].getName());
            code.append(s);

            // 為 getter 方法傳回的 URL 生成判空代碼,格式如下:
            // if (argN.getter方法名() == null)
            //     throw new IllegalArgumentException(參數全限定名 + argument getUrl() == null);
            s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                              urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
            code.append(s);

            // 生成指派語句,格式如下:
            // URL全限定名 url = argN.getter方法名(),比如
            // com.alibaba.dubbo.common.URL url = invoker.getUrl();
            s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
            code.append(s);
        }

        // 省略無關代碼
    }

    // 省略無關代碼
}           

上面代碼有點多,需要耐心看一下。這段代碼主要目的是為了擷取 URL 資料,并為之生成判空和指派代碼。以 Protocol 的 refer 和 export 方法為例,上面的代碼為它們生成如下内容(代碼已格式化):

// refer:
if (arg1 == null)
    throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;

// export:
if (arg0 == null)
    throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
if (arg0.getUrl() == null)
    throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
com.alibaba.dubbo.common.URL url = arg0.getUrl();           

Adaptive 注解值 value 類型為 String[],可填寫多個值,預設情況下為空數組。若 value 為非空數組,直接擷取數組内容即可。若 value 為空數組,則需進行額外處理。處理過程是将類名轉換為字元數組,然後周遊字元數組,并将字元放入 StringBuilder 中。若字元為大寫字母,則向 StringBuilder 中添加點号,随後将字元變為小寫存入 StringBuilder 中。比如 LoadBalance 經過處理後,得到 load.balance。

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // ${無 Adaptive 注解方法代碼生成邏輯}
    } else {
        // ${擷取 URL 資料}

        String[] value = adaptiveAnnotation.value();
        // value 為空數組
        if (value.length == 0) {
            // 擷取類名,并将類名轉換為字元數組
            char[] charArray = type.getSimpleName().toCharArray();
            StringBuilder sb = new StringBuilder(128);
            // 周遊位元組數組
            for (int i = 0; i < charArray.length; i++) {
                // 檢測目前字元是否為大寫字母
                if (Character.isUpperCase(charArray[i])) {
                    if (i != 0) {
                        // 向 sb 中添加點号
                        sb.append(".");
                    }
                    // 将字元變為小寫,并添加到 sb 中
                    sb.append(Character.toLowerCase(charArray[i]));
                } else {
                    // 添加字元到 sb 中
                    sb.append(charArray[i]);
                }
            }
            value = new String[]{sb.toString()};
        }

        // 省略無關代碼
    }

    // 省略無關邏輯
}           

此段邏輯是檢測方法清單中是否存在 Invocation 類型的參數,若存在,則為其生成判空代碼和其他一些代碼。相應的邏輯如下:

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();    // 擷取參數類型清單
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // ${無 Adaptive 注解方法代碼生成邏輯}
    } else {
        // ${擷取 URL 資料}

        // ${擷取 Adaptive 注解值}

        boolean hasInvocation = false;
        // 周遊參數類型清單
        for (int i = 0; i < pts.length; ++i) {
            // 判斷目前參數名稱是否等于 com.alibaba.dubbo.rpc.Invocation
            if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                // 為 Invocation 類型參數生成判空代碼
                String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                code.append(s);
                // 生成 getMethodName 方法調用代碼,格式為:
                //    String methodName = argN.getMethodName();
                s = String.format("\nString methodName = arg%d.getMethodName();", i);
                code.append(s);

                // 設定 hasInvocation 為 true
                hasInvocation = true;
                break;
            }
        }
    }

    // 省略無關邏輯
}           

本段邏輯用于根據 SPI 和 Adaptive 注解值生成“擷取拓展名邏輯”,同時生成邏輯也受 Invocation 類型參數影響,綜合因素導緻本段邏輯相對複雜。本段邏輯可以會生成但不限于下面的代碼:

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());

String extName = url.getMethodParameter(methodName, "loadbalance", "random");

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));           

本段邏輯複雜之處在于條件分支比較多,大家在閱讀源碼時需要知道每個條件分支的意義是什麼,否則不太容易看懂相關代碼。下面開始分析本段邏輯。

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // $無 Adaptive 注解方法代碼生成邏輯}
    } else {
        // ${擷取 URL 資料}

        // ${擷取 Adaptive 注解值}

        // ${檢測 Invocation 參數}

        // 設定預設拓展名,cachedDefaultName 源于 SPI 注解值,預設情況下,
        // SPI 注解值為空串,此時 cachedDefaultName = null
        String defaultExtName = cachedDefaultName;
        String getNameCode = null;

        // 周遊 value,這裡的 value 是 Adaptive 的注解值,前面分析過 value 變量的擷取過程。
        // 此處循環目的是生成從 URL 中擷取拓展名的代碼,生成的代碼會指派給 getNameCode 變量。注意這
        // 個循環的周遊順序是由後向前周遊的。
        for (int i = value.length - 1; i >= 0; --i) {
            // 當 i 為最後一個元素的坐标時
            if (i == value.length - 1) {
                // 預設拓展名非空
                if (null != defaultExtName) {
                    // protocol 是 url 的一部分,可通過 getProtocol 方法擷取,其他的則是從
                    // URL 參數中擷取。因為擷取方式不同,是以這裡要判斷 value[i] 是否為 protocol
                    if (!"protocol".equals(value[i]))
                     // hasInvocation 用于辨別方法參數清單中是否有 Invocation 類型參數
                        if (hasInvocation)
                            // 生成的代碼功能等價于下面的代碼:
                            //   url.getMethodParameter(methodName, value[i], defaultExtName)
                            // 以 LoadBalance 接口的 select 方法為例,最終生成的代碼如下:
                            //   url.getMethodParameter(methodName, "loadbalance", "random")
                            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        else
                            // 生成的代碼功能等價于下面的代碼:
                            //   url.getParameter(value[i], defaultExtName)
                            getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                    else
                        // 生成的代碼功能等價于下面的代碼:
                        //   ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                        getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);

                // 預設拓展名為空
                } else {
                    if (!"protocol".equals(value[i]))
                        if (hasInvocation)
                            // 生成代碼格式同上
                            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        else
                            // 生成的代碼功能等價于下面的代碼:
                            //   url.getParameter(value[i])
                            getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                    else
                        // 生成從 url 中擷取協定的代碼,比如 "dubbo"
                        getNameCode = "url.getProtocol()";
                }
            } else {
                if (!"protocol".equals(value[i]))
                    if (hasInvocation)
                        // 生成代碼格式同上
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                    else
                        // 生成的代碼功能等價于下面的代碼:
                        //   url.getParameter(value[i], getNameCode)
                        // 以 Transporter 接口的 connect 方法為例,最終生成的代碼如下:
                        //   url.getParameter("client", url.getParameter("transporter", "netty"))
                        getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                else
                    // 生成的代碼功能等價于下面的代碼:
                    //   url.getProtocol() == null ? getNameCode : url.getProtocol()
                    // 以 Protocol 接口的 connect 方法為例,最終生成的代碼如下:
                    //   url.getProtocol() == null ? "dubbo" : url.getProtocol()
                    getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
            }
        }
        // 生成 extName 指派代碼
        code.append("\nString extName = ").append(getNameCode).append(";");
        // 生成 extName 判空代碼
        String s = String.format("\nif(extName == null) " +
                                 "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                                 type.getName(), Arrays.toString(value));
        code.append(s);
    }

    // 省略無關邏輯
}           

這裡我以 Transporter 接口的自适應拓展類代碼生成過程舉例說明。首先看一下 Transporter 接口的定義,如下:

@SPI("netty")
public interface Transporter {
    // @Adaptive({server, transporter})
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    // @Adaptive({client, transporter})
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}           

下面對 connect 方法代理邏輯生成的過程進行分析,此時生成代理邏輯所用到的變量如下:

String defaultExtName = "netty";
boolean hasInvocation = false;
String getNameCode = null;
String[] value = ["client", "transporter"];           

下面對 value 數組進行周遊,此時 i = 1, value[i] = "transporter",生成的代碼如下:

getNameCode = url.getParameter("transporter", "netty");           

接下來,for 循環繼續執行,此時 i = 0, value[i] = "client",生成的代碼如下:

getNameCode = url.getParameter("client", url.getParameter("transporter", "netty"));           

for 循環結束運作,現在為 extName 變量生成指派和判空代碼,如下:

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
if (extName == null) {
    throw new IllegalStateException(
        "Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString()
        + ") use keys([client, transporter])");
}           

本段代碼邏輯用于根據拓展名加載拓展執行個體,并調用拓展執行個體的目标方法。相關邏輯如下:

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // $無 Adaptive 注解方法代碼生成邏輯}
    } else {
        // ${擷取 URL 資料}

        // ${擷取 Adaptive 注解值}

        // ${檢測 Invocation 參數}

        // ${生成拓展名擷取邏輯}

        // 生成拓展擷取代碼,格式如下:
        // type全限定名 extension = (type全限定名)ExtensionLoader全限定名
        //     .getExtensionLoader(type全限定名.class).getExtension(extName);
        // Tips: 格式化字元串中的 %<s 表示使用前一個轉換符所描述的參數,即 type 全限定名
        s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
        code.append(s);

        // 如果方法傳回值類型非 void,則生成 return 語句。
        if (!rt.equals(void.class)) {
            code.append("\nreturn ");
        }

        // 生成目标方法調用邏輯,格式為:
        //     extension.方法名(arg0, arg2, ..., argN);
        s = String.format("extension.%s(", method.getName());
        code.append(s);
        for (int i = 0; i < pts.length; i++) {
            if (i != 0)
                code.append(", ");
            code.append("arg").append(i);
        }
        code.append(");");
    }

    // 省略無關邏輯
}           

以 Protocol 接口舉例說明,上面代碼生成的内容如下:

com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader
    .getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);           

本節進行代碼生成的收尾工作,主要用于生成方法定義的代碼。相關邏輯如下:

for (Method method : methods) {
    Class<?> rt = method.getReturnType();
    Class<?>[] pts = method.getParameterTypes();
    Class<?>[] ets = method.getExceptionTypes();

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // $無 Adaptive 注解方法代碼生成邏輯}
    } else {
        // ${擷取 URL 資料}

        // ${擷取 Adaptive 注解值}

        // ${檢測 Invocation 參數}

        // ${生成拓展名擷取邏輯}

        // ${生成拓展加載與目标方法調用邏輯}
    }
}

// public + 傳回值全限定名 + 方法名 + (
codeBuilder.append("\npublic ")
    .append(rt.getCanonicalName())
    .append(" ")
    .append(method.getName())
    .append("(");

// 添加參數清單代碼
for (int i = 0; i < pts.length; i++) {
    if (i > 0) {
        codeBuilder.append(", ");
    }
    codeBuilder.append(pts[i].getCanonicalName());
    codeBuilder.append(" ");
    codeBuilder.append("arg").append(i);
}
codeBuilder.append(")");

// 添加異常抛出代碼
if (ets.length > 0) {
    codeBuilder.append(" throws ");
    for (int i = 0; i < ets.length; i++) {
        if (i > 0) {
            codeBuilder.append(", ");
        }
        codeBuilder.append(ets[i].getCanonicalName());
    }
}
codeBuilder.append(" {");
codeBuilder.append(code.toString());
codeBuilder.append("\n}");           

以 Protocol 的 refer 方法為例,上面代碼生成的内容如下:

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
    // 方法體
}           

到此,關于自适應拓展的原理,實作就分析完了。總的來說自适應拓展整個邏輯還是很複雜的,并不是很容易弄懂。

文章說明

更多有價值的文章均收錄于

貝貝貓的文章目錄
Dubbo SPI 實作

版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!

創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。

參考内容

[1]《深入了解Apache Dubbo與實戰》

[2]

dubbo 官方文檔