Dubbo原始碼學習之-Adaptive自適應擴充套件

張曾經發表於2019-07-28

前言

        最近三週基本處於9-10-6與9-10-7之間,忙碌的節奏機會丟失了自己。除了之前幹施工的那段經歷,只看參加軟體開發以來,前段時間是最繁忙的了。忙的原因,不是要完成的工作量大,而是各種環境問題,各種溝通協調問題。從這個專案,我是體會到了人一多,花在溝通協調上的成本真的會不成比例的放大,制度好,再加上協調好,會極大的提高整體工作效率。怪不得當年華為跟IBM學完工作組織管理制度之後能爆發出如此強勁的戰鬥力。從另一個角度,也能發覺出為什麼大公司招人都比較注重員工的個人實力與團隊協作能力,因為如果是多人協作的工作,一旦有人跟不上會極大的拖延整體進度,而且相對而言技術能力強的人更容易溝通交流達成共識,工作協作成本會比能力弱的人低很多。大道千條,我選其一,多提升個人能力才是王道。

        閒話少敘,下面繼續Dubbo原始碼的學習。上一節說的是Dubbo用SPI機制來進行Bean的管理與引用,類似於Spring中BeanFactory對bean的管理。SPI實現了類似於 "在容器中管理Bean"的功能,那麼問題來了,如果我想在程式執行時呼叫SPI中管理的類的方法,再通過執行時的引數來確定呼叫哪個實現類,這麼矛盾的場景應該怎麼實現?這時就要靠Dubbo的自適應擴充套件機制了。

正文

        實現的思路其實不難,我們先一起來分析一下。首先程式執行時直接呼叫的SPI管理類中的方法不是通過SPI載入的類,因為這時候還未載入,所以此時只能先通過代理類代理,在代理類的方法中再進行判斷,看需要呼叫哪個實現類,再去載入這個實現類並呼叫目標方法。即最先呼叫的那個方法只是最終要呼叫的實現類方法的一個代理而已。新增了一個代理層,就實現了一個看似矛盾的場景,從這也可以看出軟體開發的一個重要的思想武器-分層。

        但要真正實現這個思路,將它落地,還是比較複雜的。首先要確定,哪些方法需要生成代理類進行代理?Dubbo中是通過@Adaptive註解來標識類與方法實現的。其次,代理類如何生成?Dubbo中先拼接出一段java程式碼的字串,然後預設使用javassit編譯這段程式碼載入進JVM得到class物件,再利用反射生成代理類。最後,代理類生成後,通過什麼來確認最終要載入呼叫的實現類?Dubbo中對此進行了規範,統一從URL物件中獲取引數找到最終呼叫的實現類。注意此處的URL是Dubbo中自己定義的一個類,類路徑為  org.apache.dubbo.common.URL。

一、@Adaptive註解

此註解是自適應擴充套件的觸發點,可以加在類上跟方法上。加在類上,表示該類是一個擴充套件類,不需要生成代理直接用即可;加在方法上則表示該方法需生成代理。Dubbo中此註解載入類上的情況,只有兩個類:AdaptiveCompiler和AdaptiveExtensionFactory。以AdaptiveExtensionFactory為例,原始碼如下所示:

 1 @Adaptive
 2 public class AdaptiveExtensionFactory implements ExtensionFactory {
 3 
 4     private final List<ExtensionFactory> factories;
 5 
 6     public AdaptiveExtensionFactory() {
 7         ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
 8         List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
 9         for (String name : loader.getSupportedExtensions()) {
10             list.add(loader.getExtension(name));
11         }
12         factories = Collections.unmodifiableList(list);
13     }
14 
15     @Override
16     public <T> T getExtension(Class<T> type, String name) {
17         for (ExtensionFactory factory : factories) {
18             T extension = factory.getExtension(type, name);
19             if (extension != null) {
20                 return extension;
21             }
22         }
23         return null;
24     }
25 
26 }

可見其getExtension方法自己進行了實現,屬性factories中放的就是兩個類: SPIExtensionFactory跟SpringExtensionFactory,分別是Dubbo自身的SPI擴充套件工廠以及Spring的相關擴充套件工廠。

註解加在方法上的情況,以Protocol介面為例:

 1 @SPI("dubbo")
 2 public interface Protocol {
 3 
 4     int getDefaultPort();
 5 
 6     @Adaptive
 7     <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
 8 
 9     @Adaptive
10     <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
11 
12     void destroy();
13 
14 }

可見其中的export方法跟refer方法都加上了@Adaptive註解

二、代理如何生成

下面以Protocol介面中的export服務匯出方法為例,看看Dubbo原始碼中是如何實現的代理生成。

在ServiceConfig類中的doExportUrlsFor1Protocol方法中,有一段這樣的程式碼:

1 Invoker<?> invoker = PROXY_FACTORY.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(EXPORT_KEY, url.toFullString()));
2 DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker, this);
3 Exporter<?> exporter = protocol.export(wrapperInvoker);

此處就是往遠端匯出服務的觸發點。先生成了invoker,然後生成invoker的包裝類可以看到在第三行呼叫了protocol介面的export方法。protocol屬性為:

1 private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

追蹤getAdaptiveExtension()方法,最終找到生成代理類程式碼的地方:org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator#generate

 1 public String generate() {
 2         // no need to generate adaptive class since there's no adaptive method found.
 3         if (!hasAdaptiveMethod()) {
 4             throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
 5         }
 6 
 7         StringBuilder code = new StringBuilder();
 8         code.append(generatePackageInfo());
 9         code.append(generateImports());
10         code.append(generateClassDeclaration());
11         
12         Method[] methods = type.getMethods();
13         for (Method method : methods) {
14             code.append(generateMethod(method));
15         }
16         code.append("}");
17         
18         if (logger.isDebugEnabled()) {
19             logger.debug(code.toString());
20         }
21         return code.toString();
22     }

整個程式碼拼接的過程比較複雜,按照java語法拼裝各個部分,最終得到一個代理類的程式碼。具體的程式碼實現如果感興趣可以自行檢視,太多太麻煩,此處就不一一例舉了。

然後在ExtensionLoader中編譯,載入,得到Class類,方法如下所示:

1 private Class<?> createAdaptiveExtensionClass() {
2         String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
3         ClassLoader classLoader = findClassLoader();
4         org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
5         return compiler.compile(code, classLoader);
6     }

最後用反射例項化,得到代理類物件。

三、根據URL載入指定的SPI實現類,呼叫方法

此步是在代理類的程式碼拼接中實現的。追蹤上述generate()方法中的generateMethod(method)方法:

1 private String generateMethod(Method method) {
2         String methodReturnType = method.getReturnType().getCanonicalName();
3         String methodName = method.getName();
4         String methodContent = generateMethodContent(method);
5         String methodArgs = generateMethodArguments(method);
6         String methodThrows = generateMethodThrows(method);
7         return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
8     }

可見此處將一個方法分成了五部分:方法返回值、方法名、方法內容、方法引數、方法異常。分別獲得這5部分後再拼接,組成一個完整的方法。

其餘都比較簡單,主要關注方法內容的獲取 generateMethodContent()方法。

 1 private String generateMethodContent(Method method) {
 2         Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
 3         StringBuilder code = new StringBuilder(512);
 4         if (adaptiveAnnotation == null) {
 5             return generateUnsupported(method);
 6         } else {
 7             int urlTypeIndex = getUrlTypeIndex(method);
 8             
 9             // found parameter in URL type
10             if (urlTypeIndex != -1) {
11                 // Null Point check
12                 code.append(generateUrlNullCheck(urlTypeIndex));
13             } else {
14                 // did not find parameter in URL type
15                 code.append(generateUrlAssignmentIndirectly(method));
16             }
17 
18             String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
19 
20             boolean hasInvocation = hasInvocationArgument(method);
21             
22             code.append(generateInvocationArgumentNullCheck(method));
23             
24             code.append(generateExtNameAssignment(value, hasInvocation));
25             // check extName == null?
26             code.append(generateExtNameNullCheck(value));
27             
28             code.append(generateExtensionAssignment());
29 
30             // return statement
31             code.append(generateReturnAndInvocation(method));
32         }
33         
34         return code.toString();
35     }

由於Dubbo統一規定通過URL來獲取動態載入的類的key,所以我們要帶著這樣的設計前提來看這個方法。

首先getUrlTypeIndex這個方法是用來判斷當前方法的引數中有沒有URL,如果有的話返回值就是URL引數在整個引數列表中的下標位置,沒有的話返回-1。

由於Protocol的export方法引數中沒有URL,所以此處應該進入else中的方法 generateUrlAssignmentIndirectly() 中。此方法是去找到引數中的getUrl方法,然後獲取到。執行完此方法後得到的內容為:

1 if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
2 if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
3 org.apache.dubbo.common.URL url = arg0.getUrl();

執行generateExtNameAssignment方法後得到的結果為:

1 String extName = url.getProtocol();

執行generateExtensionAssignment方法得到的結果為:

1 org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);

 執行generateReturnAndInvocation方法得到的結果為:

1 return extension.export(arg0);

這樣,代理類的程式碼便拼湊出來了,後面通過編譯類編譯、載入進JVM、得到例項物件就可一氣呵成的完成了。

尾聲

        至此,便完成了Dubbo的自適應擴充套件機制。可以發現,整個過程沒有什麼難的地方,大都是平常用過或者見過的用法,但是經優秀的阿里中介軟體工程師們之手一組合,就可以實現如此的功能,很讓人佩服。下一期是Dubbo的服務匯出功能解讀,敬請關注。

 

相關文章