前言
最近三周基本處於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的服務導出功能解讀,敬請關注。