dubbo系列(二) 自適應SPI

1. 介紹

  在上一篇博客中,我們介紹了SPI的基本使用,通過指定擴展點類型,可以創建擴展點的實現類,但是在dubbo中,有些時候並不期望直接創建出一個具體的擴展點實現,而是期望創建出一個未知類型的擴展點實現,在調用這個未知類型擴展點的擴展方法時,通過參數判斷具體創建哪個類型的擴展點實現,然後在執行具體方法,看起來有些繞,請看接下來的具體使用。

2.自適應擴展點示例

  在擴展點接口中新增加一個speak(Url url)接口,並標註@Adaptive註解,Url是Dubbo的總線,我們dubbo就是通過獲取url中的屬性,來決定加載哪一個具體的SPI實現,url中的參數是key-value對,@Adaptive後面的friend表示獲取url中key爲friend的value值。

@SPI
public interface Animal {
    public void speak();

    @Adaptive("friend")
    public void speak(URL url);
}

  SPI實現類

public class Cat implements Animal{
    @Override
    public void speak() {
        System.out.println("I'm a cat");
    }

    @Override
    public void speak(URL url) {
        System.out.println("I'm cat");
    }
}
public class Dog implements Animal{
    @Override
    public void speak() {
        System.out.println("I'm a dog");
    }

    @Override
    public void speak(URL url) {
        System.out.println("I'm a dog");
    }
}

  測試類:

public static void main(String[] args) {
   	ExtensionLoader<Animal> extensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
    Animal animal = extensionLoader.getAdaptiveExtension();

    Map<String,String> map = new HashMap<>();
    map.put("friend","dog");
    URL url = new URL("","",1,map);

    animal.speak(url);
}

  運行結果:I’m a dog

  正如介紹中所說,自適應擴展點,是指創建除了一個不確定類型的擴展點,通過extensionLoader.getAdaptiveExtension(),該方法我們並沒有指定類型,我們構建了url參數,在url中執行了dog類型,因此最終通過自適應擴展點執行speak(url)方法時,加載了Dog擴展點,並執行,這和上一章節的用法比起來,我們將具體SPI實現類的加載時機,從創建階段推遲到了方法的執行階段,屬於懶加載的一種實現思路。

3.自適應擴展點到底是什麼

  通過extensionLoader.getAdaptiveExtension();創建出來的Animi到底什麼,通過一些工具我們可以獲取到這個對象的類結構,具體如下:

package com.hy.study.spi;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Animal$Adaptive implements com.hy.study.spi.Animal {
public void speak() {throw new UnsupportedOperationException("method public abstract void com.hy.study.spi.Animal.speak() of interface com.hy.study.spi.Animal is not adaptive method!");
}
public void speak(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("friend");
if(extName == null) throw new IllegalStateException("Fail to get extension(com.hy.study.spi.Animal) name from url(" + url.toString() + ") use keys([friend])");
com.hy.study.spi.Animal extension = (com.hy.study.spi.Animal)ExtensionLoader.getExtensionLoader(com.hy.study.spi.Animal.class).getExtension(extName);extension.speak(arg0);
}
}

  我們看到speak(url)這個方法由於標註了@Adaptive註解,因此dubbo幫我們實現了方法體,方法體中從url的friend參數獲取值,然後又執行了getExtension獲取了具體的SPI實現,因此真相大白。

4.源碼分析

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

4.1 獲取自適應拓展

  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 實例。接下來,我們把重點放在代理類代碼生成的邏輯上,其他邏輯大家自行分析。

4.2 自適應拓展類代碼生成

  createAdaptiveExtensionClassCode 方法代碼略多,約有兩百行代碼。因此本節將會對該方法的代碼進行拆分分析,以幫助大家更好的理解代碼邏輯。

2.2.1 Adaptive 註解檢測

  在生成代理類源碼之前,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 ...");
4.2.2 生成類

  通過 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 {
    // 省略方法代碼
}
4.2.3 生成方法

  一個方法可以被 Adaptive 註解修飾,也可以不被修飾。這裏將未被 Adaptive 註解修飾的方法稱爲“無 Adaptive 註解方法”,下面我們先來看看此種方法的代碼生成邏輯是怎樣的。
4.2.3.1 無 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!");

4.2.3.2 獲取 URL 數據

  前面說過方法代理邏輯會從 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();

4.2.3.3 獲取 Adaptive 註解值
  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()};
        }
        
        // 省略無關代碼
    }
    
    // 省略無關邏輯
}

4.2.3.4 檢測 Invocation 參數
  此段邏輯是檢測方法列表中是否存在 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;
            }
        }
    }
    
    // 省略無關邏輯
}

2.2.3.5 生成拓展名獲取邏輯
  本段邏輯用於根據 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 的註解值,2.2.3.3 節分析過 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);
    }
    
    // 省略無關邏輯
}

  上面代碼比較複雜,不是很好理解。對於這段代碼,建議大家寫點測試用例,對 Protocol、LoadBalance 以及 Transporter 等接口的自適應拓展類代碼生成過程進行調試。這裏我以 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])");
}

4.2.3.6 生成拓展加載與目標方法調用邏輯
本段代碼邏輯用於根據拓展名加載拓展實例,並調用拓展實例的目標方法。相關邏輯如下:

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

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

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) {
    // 方法體
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章