Dubbo原始碼分析之SPI(三)
一、概述
本篇介紹自適應擴充套件,方法getAdaptiveExtension()的實現。ExtensionLoader類本身很多功能也使用到了自適應擴充套件。包括ExtensionFactory擴充套件。
通俗的講,自適應擴充套件要實現的邏輯是:呼叫擴充套件點的方法時,自動判斷要呼叫那個擴充套件點實現類的方法。我們知道,一個擴充套件點通常有多個實現類,在配置文字檔案中分多行配置,在前面的分析中,我們知道通過getExtension(String name)方法,返回的是指定key的擴充套件點,而自適應擴充套件點方法getAdaptiveExtension()在呼叫前,不確認返回那個擴充套件點。而是在方法呼叫的時候,根據方法入參,進行確定,具體是呼叫那個實現類。
自適應擴充套件基於@Adaptive註解,可以修飾類,也可以修飾方法。修飾類的時候,邏輯比較簡單,不會動態生成程式碼邏輯,使用的場景也比較少,主要包括AdaptiveCompiler 和 AdaptiveExtensionFactory。修飾方法的時候,會動態生成一個新類,新類包括擴充套件點的所有方法,呼叫getAdaptiveExtension()返回的就是新類物件。
二、詳細介紹
前面我們說過,@Adaptive可以修飾類,也可以修飾方法。我們先看下修飾類的場景。
通過一個具體的實現類來看下,這裡我們分析AdaptiveCompiler類的實現:
1 @Adaptive 2 public class AdaptiveCompiler implements Compiler { 3 4 private static volatile String DEFAULT_COMPILER; 5 6 public static void setDefaultCompiler(String compiler) { 7 DEFAULT_COMPILER = compiler; 8 } 9 10 @Override 11 public Class<?> compile(String code, ClassLoader classLoader) { 12 Compiler compiler; 13 ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class); 14 String name = DEFAULT_COMPILER; // copy reference 15 if (name != null && name.length() > 0) { 16 compiler = loader.getExtension(name); 17 } else { 18 compiler = loader.getDefaultExtension(); 19 } 20 return compiler.compile(code, classLoader); 21 } 22 23 }
此類,只有一個對外提供的方法compile(String code, ClassLoader classLoader),我們來看方法的實現。
首先溝通ExtensionLoader.getExtensionLoader(Compiler.class),獲取Compiler對應的ExtensionLoader物件,然後判斷構造器中初始化的DEFAULT_COMPILER 變數是否有值。如果存在就通過loader.getExtension(name)方法獲得擴充套件點實現。如果DEFAULT_COMPILER 為空,則呼叫loader.getDefaultExtension()方法,返回預設實現。獲取compiler擴充套件點實現物件後,呼叫對應的compile方法。
由此,我們可以看到,@Adaptive修飾的類,在呼叫具體方法的時候,是根據一定的條件進行判斷,確認具體呼叫的實現類物件。
我們再說下@Adaptive修飾方法的場景。
擴充套件點實現類的方法如果被@Adaptive修飾,在呼叫getAdaptiveExtension()方法時候,程式會自動生成一個新類,新類是一個名為擴充套件點介面名+$Adaptive,實現了擴充套件點介面的類。新類中的方法,主要分為兩類,一是有@Adaptive註解的方法,一個是沒有@Adaptive註解的方法。
有@Adaptive註解的方法,方法內部會判斷方法入參是否有URL(此處是dubbo內的URL),或是方法入參物件是否可以get到URL。如果都不能獲取到URL,直接throw 出Exception。如果能獲取到URL,則從URL物件中獲取需要呼叫的實現類對應的配置文字檔案的key,根據什麼引數從URL中獲取呢?首先是從@Adaptive的value獲取(此value是一個字串陣列),如果@Adaptive為空,則根據類名進行轉換,得出從URL獲取key的引數名,轉換規則是根據駝峰規則,遇到大寫字元新增”.“,如 AdaptiveFactory 為:adaptive.factory。獲得引數後,再通過getExtension(..)方法,獲得需要呼叫的擴充套件點實現類物件。
到這裡,我們基本介紹了自適應擴充套件點的實現邏輯,但是有一點沒有說到,就是不管@Adaptive修飾類還是修飾方法,自適應擴充套件點的返回邏輯,這點是要結合程式碼進行說明,接下來就開啟我們的原始碼分析。
三、原始碼分析
我們從getAdaptiveExtension()方法開始
1 public T getAdaptiveExtension() { 2 // 從快取中獲取自定義拓展 3 Object instance = cachedAdaptiveInstance.get(); 4 if (instance == null) { 5 if (createAdaptiveInstanceError == null) { 6 synchronized (cachedAdaptiveInstance) { 7 instance = cachedAdaptiveInstance.get(); 8 if (instance == null) { 9 try { 10 instance = createAdaptiveExtension(); 11 cachedAdaptiveInstance.set(instance); 12 } catch (Throwable t) { 13 createAdaptiveInstanceError = t; 14 throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t); 15 } 16 } 17 } 18 } else { 19 throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError); 20 } 21 } 22 23 return (T) instance; 24 }
這個方法的邏輯很簡單,主要包括
1、從快取物件cachedAdaptiveInstance獲取自適應擴充套件點例項
2、快取有直接返回,沒有進行方法createAdaptiveExtension()呼叫
3、根據方法返回的例項,設定到到快取裡,並進行返回
所以我們接著分析createAdaptiveExtension方法
1 private T createAdaptiveExtension() { 2 try { 3 // injectExtension 為@Adaptive註解的類 可能存在的IOC服務 4 // @Adaptive註解方法 自動生成的代理類不存在IOC可能 5 T instance = (T) getAdaptiveExtensionClass().newInstance(); 6 return injectExtension(instance); 7 } catch (Exception e) { 8 throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e); 9 } 10 }
可以看到,方法內部是通過getAdaptiveExtensionClass() 獲取到Class例項,再反射例項化,獲取到例項物件。
我們接著往下看getAdaptiveExtensionClass()方法
1 private Class<?> getAdaptiveExtensionClass() { 2 // 通過SPI獲取所有的擴充套件類,賦值相關的成員變數 3 getExtensionClasses(); 4 // 如果有@Adaptive修飾的類,cachedAdaptiveClass不為空 5 if (cachedAdaptiveClass != null) { 6 return cachedAdaptiveClass; 7 } 8 // 沒有@Adaptive修飾的類時,根據@Adaptive修飾方法 建立自適應擴充套件類 9 return cachedAdaptiveClass = createAdaptiveExtensionClass(); 10 }
首先執行的是getExtensionClasses()方法,之後判斷cachedAdaptiveClass 是否為空,不為空就直接返回了。這個cachedAdaptiveClass 變數,其實就是有@Adaptive修飾的擴充套件點實現。也就是說,如果在擴充套件點的實現類中,存在@Adaptive修飾的類,就直接返回這個類了。
那麼cachedAdaptiveClass 在是哪裡賦值的呢?我們需要再看getExtensionClasses()方法。getExtensionClasses這個方法在前面兩篇文章中已經都有介紹。在預設擴充套件點的實現裡面,cachedDefaultName變數的賦值就是在這個方法裡進行的。cachedAdaptiveClass 的賦值的方法呼叫鏈我們這裡直接給出來
1 getExtensionClasses()-->loadExtensionClasses()-->loadDirectory()-->loadResource()-->loadClass()
隱藏的比較深,第5個方法才對cachedDefaultName進行了賦值。
我們一步一步來分析,先看getExtensionClasses()
1 private Map<String, Class<?>> getExtensionClasses() { 2 Map<String, Class<?>> classes = cachedClasses.get(); 3 if (classes == null) { 4 synchronized (cachedClasses) { 5 classes = cachedClasses.get(); 6 if (classes == null) { 7 classes = loadExtensionClasses(); 8 cachedClasses.set(classes); 9 } 10 } 11 } 12 return classes; 13 }
這個方法很簡單,我們接著看loadExtensionClasses()
1 private Map<String, Class<?>> loadExtensionClasses() { 2 // 獲取註解 SPI的介面 3 // type為傳入的擴充套件介面,必須有@SPI註解 4 final SPI defaultAnnotation = type.getAnnotation(SPI.class); 5 // 獲取預設擴充套件實現value,如果存在,賦值給cachedDefaultName 6 if (defaultAnnotation != null) { 7 String value = defaultAnnotation.value(); 8 if ((value = value.trim()).length() > 0) { 9 // @SPI value 只能是一個,不能為逗號分割的多個 10 // @SPI value為預設的擴充套件實現 11 String[] names = NAME_SEPARATOR.split(value); 12 if (names.length > 1) { 13 throw new IllegalStateException("more than 1 default extension name on extension " + type.getName() + ": " + Arrays.toString(names)); 14 } 15 if (names.length == 1) 16 cachedDefaultName = names[0]; 17 } 18 } 19 // 載入三個目錄配置的擴充套件類 20 Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); 21 // META-INF/dubbo/internal 22 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); 23 // META-INF/dubbo 24 loadDirectory(extensionClasses, DUBBO_DIRECTORY); 25 // META-INF/services/ 26 loadDirectory(extensionClasses, SERVICES_DIRECTORY); 27 return extensionClasses; 28 }
很熟悉吧,這個方法我們在第一篇文章中已經有介紹了,我們再接著往下看loadDirectory方法
1 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) { 2 // 擴充套件配置檔案完整檔案路徑+檔名 3 String fileName = dir + type.getName(); 4 try { 5 6 Enumeration<java.net.URL> urls; 7 // 獲取類載入器 8 ClassLoader classLoader = findClassLoader(); 9 if (classLoader != null) { 10 urls = classLoader.getResources(fileName); 11 } else { 12 urls = ClassLoader.getSystemResources(fileName); 13 } 14 if (urls != null) { 15 while (urls.hasMoreElements()) { 16 java.net.URL resourceURL = urls.nextElement(); 17 // 載入 18 loadResource(extensionClasses, classLoader, resourceURL); 19 } 20 } 21 } catch (Throwable t) { 22 logger.error("Exception when load extension class(interface: " + type + ", description file: " + fileName + ").", t); 23 } 24 }
這個方法我們也很分析過了,再往下看loadResource()方法
1 private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { 2 try { 3 BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8")); 4 try { 5 String line; 6 while ((line = reader.readLine()) != null) { 7 // 字元#是註釋開始標誌,只取#前面的字元 8 final int ci = line.indexOf('#'); 9 if (ci >= 0) 10 line = line.substring(0, ci); 11 line = line.trim(); 12 if (line.length() > 0) { 13 try { 14 String name = null; 15 int i = line.indexOf('='); 16 if (i > 0) { 17 // 解析出 name 和 實現類 18 name = line.substring(0, i).trim(); 19 line = line.substring(i + 1).trim(); 20 } 21 if (line.length() > 0) { 22 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); 23 } 24 } catch (Throwable t) { 25 IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); 26 exceptions.put(line, e); 27 } 28 } 29 } 30 } finally { 31 reader.close(); 32 } 33 } catch (Throwable t) { 34 logger.error("Exception when load extension class(interface: " + type + ", class file: " + resourceURL + ") in " + resourceURL, t); 35 } 36 }
這裡是解析配置文字檔案的內容,通過反射獲得Class,再呼叫loadClass(),我們接著往下
1 private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException { 2 // type是否為clazz的超類,clazz是否實現了type介面 3 // 此處clazz 是擴充套件實現類的Class 4 if (!type.isAssignableFrom(clazz)) { 5 throw new IllegalStateException("Error when load extension class(interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + "is not subtype of interface."); 6 } 7 // clazz是否註解了 Adaptive 自適應擴充套件 8 // 不允許多個類註解Adaptive 9 // 註解adaptive的實現類,賦值給cachedAdaptiveClass 10 if (clazz.isAnnotationPresent(Adaptive.class)) { 11 if (cachedAdaptiveClass == null) { 12 cachedAdaptiveClass = clazz; 13 // 不允許多個實現類都註解@Adaptive 14 } else if (!cachedAdaptiveClass.equals(clazz)) { 15 throw new IllegalStateException("More than 1 adaptive class found: " + cachedAdaptiveClass.getClass().getName() + ", " + clazz.getClass().getName()); 16 } 17 // 是否為包裝類,判斷擴充套件類是否提供了引數是擴充套件點的建構函式 18 } else if (isWrapperClass(clazz)) { 19 Set<Class<?>> wrappers = cachedWrapperClasses; 20 if (wrappers == null) { 21 cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); 22 wrappers = cachedWrapperClasses; 23 } 24 wrappers.add(clazz); 25 // 普通擴充套件類 26 } else { 27 // 檢測 clazz 是否有預設的構造方法,如果沒有,則丟擲異常 28 clazz.getConstructor(); 29 // 此處name為 SPI配置中的key 30 // @SPI配置中key可以為空,此時key為擴充套件類的類名(getSimpleName())小寫 31 if (name == null || name.length() == 0) { 32 // 相容舊版本 33 name = findAnnotationName(clazz); 34 if (name.length() == 0) { 35 throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); 36 } 37 } 38 // 逗號分割 39 String[] names = NAME_SEPARATOR.split(name); 40 if (names != null && names.length > 0) { 41 // 獲取Activate註解 42 Activate activate = clazz.getAnnotation(Activate.class); 43 if (activate != null) { 44 cachedActivates.put(names[0], activate); 45 } 46 for (String n : names) { 47 if (!cachedNames.containsKey(clazz)) { 48 cachedNames.put(clazz, n); 49 } 50 // name不能重複 51 Class<?> c = extensionClasses.get(n); 52 if (c == null) { 53 extensionClasses.put(n, clazz); 54 } else if (c != clazz) { 55 throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName()); 56 } 57 } 58 } 59 } 60 }
我們在第10行,終於看到了Adaptive註解判斷。
如果擴充套件點實現類存在@Adaptive註解,Class物件賦值給cachedAdaptiveClass,並且在第14行判斷是否存在多個類都是@Adaptive註解,如果同一個擴充套件點的多個實現類都有@Adaptive註解,則丟擲異常。
到這裡,我們看到了擴充套件點自適應擴充套件點的類級別註解的呼叫及返回邏輯。其實前面也說過了,@Adaptive修飾類的場景並不多,也不是重點,重點是@Adaptive修飾方法的時候。
我們返回到getAdaptiveExtensionClass()方法,為了清晰,我們再看看這個方法的程式碼
1 private Class<?> getAdaptiveExtensionClass() { 2 // 通過SPI獲取所有的擴充套件類,賦值相關的成員變數 3 getExtensionClasses(); 4 // 如果有@Adaptive修飾的類,cachedAdaptiveClass不為空 5 if (cachedAdaptiveClass != null) { 6 return cachedAdaptiveClass; 7 } 8 // 沒有@Adaptive修飾的類時,根據@Adaptive修飾方法 建立自適應擴充套件類 9 return cachedAdaptiveClass = createAdaptiveExtensionClass(); 10 }
通過前面的分析,如果@Adaptive沒有修飾類,則cachedAdaptiveClass 為空,此時,我們會進入createAdaptiveExtensionClass(),這個方法是實現@Adaptive修飾方法的邏輯實現,也是自適應擴充套件的重點所在。
我們來看createAdaptiveExtensionClass這個方法
1 private Class<?> createAdaptiveExtensionClass() { 2 // 建立自適應擴充套件程式碼 字串 3 String code = createAdaptiveExtensionClassCode(); 4 ClassLoader classLoader = findClassLoader(); 5 // 獲取編譯器實現類 6 com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); 7 // 編譯程式碼,獲取自適應擴充套件類的Class 8 return compiler.compile(code, classLoader); 9 }
這個方法實現的功能主要兩點:
1、通過createAdaptiveExtensionClassCode()獲取建立的新類字串
2、通過Compiler編譯器,編譯新類字串,獲得新類的Class物件
所以,我們的重點是分析新類字串的實現邏輯,這也是自適應擴充套件的重點。
我們接著看createAdaptiveExtensionClassCode()方法,這個方法有300多行,我們分段來看
1 private String createAdaptiveExtensionClassCode() { 2 StringBuilder codeBuilder = new StringBuilder(); 3 // 通過反射獲取所有方法 4 Method[] methods = type.getMethods(); 5 boolean hasAdaptiveAnnotation = false; 6 // 遍歷方法,判斷至少有一個方法被@Adaptive修飾 7 for (Method m : methods) { 8 if (m.isAnnotationPresent(Adaptive.class)) { 9 hasAdaptiveAnnotation = true; 10 break; 11 } 12 } 13 // no need to generate adaptive class since there's no adaptive method found. 14 // 沒有被@Adaptive修飾的方法,丟擲異常 15 if (!hasAdaptiveAnnotation) 16 throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); 17 // 生成package程式碼:package+type所在包 18 codeBuilder.append("package ").append(type.getPackage().getName()).append(";"); 19 // 生成import程式碼:import+ExtensionLoader許可權定名 20 codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";"); 21 // 生成類程式碼:public class + type簡單名稱+$Adaptive+implements + type許可權定名+{ 22 codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {"); 23 ............... 24 ...暫時省略.... 25 .............. 26 }
方法開始的邏輯很簡單,拿到擴充套件點type的方法,迴圈方法列表,判斷是否存在一個方法是被@Adaptive註解的,如果存在則繼續,否則丟擲異常。這也符合正常的邏輯,如果所有的方法都沒@Adaptive註解,那麼獲取自適應擴充套件就沒有意義了。
從15行開始進行新類字串的構造,我們看到了關鍵字”package“、”import“、”class“等,執行到22行處,生成的程式碼字串,我們以擴充套件點Protocol為例,展示一下:
1 package com.alibaba.dubbo.rpc; 2 import com.alibaba.dubbo.common.extension.ExtensionLoader; 3 public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol { 4 // 省略方法程式碼 5 }
這個類就是我們生成的新的擴充套件點實現類,我們可以看到類名以及實現的介面。
我們接著往下分析,前面我們說過,擴充套件點方法分為兩種,一個是有@Adaptive註解的,一個是無@Adaptive註解的,既然實現了擴充套件點介面,這兩種方法都要在新類中實現。
我們首先分析沒有@Adaptive註解的方法。
1 private String createAdaptiveExtensionClassCode() { 2 ............... 3 ...暫時省略.... 4 .............. 5 // 生成方法 6 for (Method method : methods) { 7 // 方法返回型別 8 Class<?> rt = method.getReturnType(); 9 // 方法引數陣列 10 Class<?>[] pts = method.getParameterTypes(); 11 // 方法異常陣列 12 Class<?>[] ets = method.getExceptionTypes(); 13 // 方法的Adaptive註解 14 Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class); 15 StringBuilder code = new StringBuilder(512); 16 // 沒有@Adaptive註解的方法,生成的方法體內為 throw 異常 17 if (adaptiveAnnotation == null) { 18 // throw new UnsupportedOperationException( 19 // "method " + 方法簽名 + of interface + 全限定介面名 + is not adaptive method!”) 20 code.append("throw new UnsupportedOperationException(\"method ").append(method.toString()).append(" of interface ").append(type.getName()).append(" is not adaptive method!\");"); 21 } else { 22 ............... 23 ...暫時省略.... 24 .............. 25 }
通過for迴圈,進行逐個方法生成。
在第14行,獲取到方法的@Adaptive註解,第17行進行判斷,如果為空,生成的程式碼是丟擲一個異常,示例如下:
1 throw new UnsupportedOperationException( 2 "method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
我們接著分析存在@Adaptive註解的方法,生成程式碼的邏輯
1 ...暫時省略.... 2 if (adaptiveAnnotation == null) { 3 // throw new UnsupportedOperationException( 4 // "method " + 方法簽名 + of interface + 全限定介面名 + is not adaptive method!”) 5 code.append("throw new UnsupportedOperationException(\"method ").append(method.toString()) 6 .append(" of interface ").append(type.getName()).append(" is not adaptive method!\");"); 7 } else { 8 // 有@Adaptive註解的方法,引數中必須有URL,或是可以從方法引數中獲取URL 9 int urlTypeIndex = -1; 10 for (int i = 0; i < pts.length; ++i) { 11 if (pts[i].equals(URL.class)) { 12 urlTypeIndex = i; 13 break; 14 } 15 } 16 // found parameter in URL type 17 // 方法中存在URL 18 if (urlTypeIndex != -1) { 19 // Null Point check 20 // 為URL型別引數判斷空程式碼,格式如下: 21 // if (arg + urlTypeIndex == null) 22 // throw new IllegalArgumentException("url == null"); 23 String s = String.format( 24 "\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");", urlTypeIndex); 25 code.append(s); 26 // 為URL型別引數生成賦值程式碼,例如:URL url = arg1; 27 s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex); 28 code.append(s); 29 } 30 ...暫時省略....
第10行,迴圈的變數pts 為方法引數陣列,如果引數陣列中有URL型別,陣列下標賦值給urlTypeIndex,跳出迴圈。
第18行進行urlTypeIndex 判斷,此時如果不為-1,說明方法引數陣列中存在URL型別的引數,生成的程式碼首先是非空判斷,接著就是把對應的URL型別引數就行變數賦值。
接著往下看
1 ...暫時省略.... 2 else { 3 String attribMethod = null; 4 // find URL getter method 5 // 遍歷方法的引數型別 6 LBL_PTS: for (int i = 0; i < pts.length; ++i) { 7 // 方法引數型別的方法陣列 8 Method[] ms = pts[i].getMethods(); 9 for (Method m : ms) { 10 String name = m.getName(); 11 // 1、方法名以get開頭,或方法名大於3個字元 12 // 2、方法的訪問許可權是public 13 // 3、非靜態方法 14 // 4、方法引數數量為0 15 // 5、方法返回值型別為URL 16 if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers()) 17 && !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0 18 && m.getReturnType() == URL.class) { 19 urlTypeIndex = i; 20 attribMethod = name; 21 // 結束for (int i = 0; i < pts.length; ++i)迴圈 22 break LBL_PTS; 23 } 24 } 25 } 26 ...暫時省略....
上面的程式碼是引數陣列中不存在URL型別引數的情況。如果不存在URL型別的引數,就需要從所有的入參中判斷,引數物件中是否可以通過get方法 獲取到URL物件。如果不可以則丟擲異常。
標籤LBL_PTS用於結束最外部的迴圈。我們看到最外邊的迴圈,還是引數陣列pts。
第8行是拿到引數陣列中單個引數的所有方法,再進行迴圈,判斷是否存在滿足如下條件: 1、方法名以get開頭,或方法名大於3個字元;2、方法的訪問許可權是public; 3、非靜態方法;4、方法引數數量為0; 5、方法返回值型別為URL的方法,如果存在賦值方法名給attribMethod ,跳出最外變迴圈。
我們接著往下看
1 ...暫時省略.... 2 // 如果引數中都不包含可返回的URL的get方法,丟擲異常 3 if (attribMethod == null) { 4 throw new IllegalStateException("fail to create adaptive class for interface " + type.getName() 5 + ": not found url parameter or url attribute in parameters of method " 6 + method.getName()); 7 } 8 ...暫時省略....
我們看到,如果attribMethod 為空,也就是前面的兩個迴圈沒有找到存在返回URL方法的引數物件,直接丟擲異常,方法結束執行。
如果attribMethod 不為空,即存在返回URL方法的引數物件,我們再往下看:
1 ...暫時省略.... 2 // Null point check 3 // 為可返回URL的引數生成判空程式碼,格式如下: 4 // if (arg + urlTypeIndex == null) 5 // throw new IllegalArgumentException("引數全限定名 + argument == null"); 6 String s = String.format( 7 "\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");", 8 urlTypeIndex, pts[urlTypeIndex].getName()); 9 code.append(s); 10 // 為 getter 方法返回的 URL 生成判空程式碼,格式如下: 11 // if (argN.getter方法名() == null) 12 // throw new IllegalArgumentException(引數全限定名 + argument getUrl() == null); 13 s = String.format( 14 "\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");", 15 urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod); 16 code.append(s); 17 // 生成賦值語句,格式如下: 18 // URL全限定名 url = argN.getter方法名(),比如 19 // com.alibaba.dubbo.common.URL url = invoker.getUrl(); 20 s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod); 21 code.append(s); 22 ...暫時省略....
第6行生成新類程式碼是空判斷,如果引數陣列下標為urlTypeIndex的引數為空,丟擲異常。
第13行是返回URL型別方法的空判斷,我們知道引數陣列下標是urlTypeIndex的引數,存在返回URL型別的方法,方法名為attribMethod,此處就是判斷方法attribMethod是否為空null。
第20行就是執行attribMethod,賦值給url變數。
至此,從入參中獲得了URL型別的變數。前面是直接從引數陣列中獲取型別為URL的引數,後面是從引數陣列中的某個可以返回URL引數方法的引數。兩種方式目的就是獲取到URL型別的變數,這個是必須的,因為自適應的擴充套件,獲取擴充套件點的key是從URL中解析出來的。
在獲取到URL型別的變數後,現在就要獲取關鍵字key了,根據key從URL中獲取的value,就是自適應擴充套件點在配置文字檔案中對應的key。
我們接著往下看
1 ...暫時省略.... 2 // 獲取方法@Adaptive的註解值 3 String[] value = adaptiveAnnotation.value(); 4 // value is not set, use the value generated from class name as the key 5 // @Adaptive的value為空,需要特殊處理 6 // 將類名轉換為字元陣列,然後遍歷字元陣列,並將字元存入StringBulder 7 // 若字元為大寫字母,則向StringBuiilder中新增“.”,隨後字元變為小寫存入StringBuilder 8 // 比如LoadBalance經過處理,得到load.balance 9 if (value.length == 0) { 10 // 獲取類名,並將類名轉換為字元陣列 11 char[] charArray = type.getSimpleName().toCharArray(); 12 StringBuilder sb = new StringBuilder(128); 13 // 遍歷字元陣列 14 for (int i = 0; i < charArray.length; i++) { 15 // 判斷大小寫 16 if (Character.isUpperCase(charArray[i])) { 17 if (i != 0) { 18 // 大寫字元時,加 19 sb.append("."); 20 } 21 // 轉為小寫 22 sb.append(Character.toLowerCase(charArray[i])); 23 } else { 24 sb.append(charArray[i]); 25 } 26 } 27 value = new String[] { sb.toString() }; 28 } 29 ...暫時省略....
第3行,是直接從@Adaptive註解中獲取value,型別為字串陣列。
如果@Adaptive註解沒有設定value的值,接著看第9行的判斷。
從第11行開始,自動生成從URL獲取自適應擴充套件關鍵字的key。生成的邏輯是根據擴充套件點type的名稱,遍歷type名稱的字元陣列,除了首字元,遇到大寫的字元前面加“.",大寫轉小寫,組裝的字串就是要獲取的value值。
我們接著往下看
1 ...暫時省略.... 2 // 檢測方法列表中是否存在Invocation型別的引數 3 // 若存在,則為其生成判空程式碼和其他一些程式碼 4 boolean hasInvocation = false; 5 for (int i = 0; i < pts.length; ++i) { 6 // 判斷引數名稱是否等於 com.alibaba.dubbo.rpc.Invocation 7 if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) { 8 // Null Point check 9 // 為Invocation 型別引數生成判空程式碼 10 String s = String.format( 11 "\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i); 12 code.append(s); 13 // 生成getMethodName方法呼叫程式碼,格式為: 14 // String methodName = argN.getMethodName(); 15 s = String.format("\nString methodName = arg%d.getMethodName();", i); 16 code.append(s); 17 // 設定hasInvocation為true 18 hasInvocation = true; 19 break; 20 } 21 } 22 ...暫時省略....
這段程式碼選好pts,判斷是否存在型別為Invocation的引數。如果存在生成為空判斷,之後從Invocation型別的引數中獲取methodName。並設定hasInvocation為true。
1 ...暫時省略.... 2 /** 3 * 根據SPI和Adaptive註解值生成“獲取副檔名邏輯”,同時生成邏輯也受Invocation型別引數影響 生成格式如: String extName = 4 * url.getMethodParameter(methodName, "loadbalance","random"); 5 */ 6 // 設定預設副檔名,cachedDefaultName源於SPI註解值,預設情況下, 7 // SPI註解值為空串,此時cachedDefaultName 為 null 8 String defaultExtName = cachedDefaultName; 9 String getNameCode = null; 10 // 遍歷value,value是Adaptive的註解值,上面有value的獲取過程 11 // 此處迴圈的目的是生成從URL中獲取副檔名的程式碼,生成的程式碼會賦值給getNameCode變數 12 // 這個迴圈的遍歷順序是由後向前遍歷的 13 for (int i = value.length - 1; i >= 0; --i) { 14 // i為最後一個元素的座標時 15 if (i == value.length - 1) { 16 // 預設副檔名非空 17 if (null != defaultExtName) { 18 // protocol是副檔名的一部分,可以通過getProtocol方法獲取,其他則是從URL引數中獲取 19 // 因為獲取方式不同,因此要進行判斷 20 if (!"protocol".equals(value[i])) { 21 // hasInvocation 用於標識方法引數列表中是否有Invocation型別引數 22 if (hasInvocation) { 23 // 生成的程式碼功能等價於下面的程式碼: 24 // url.getMethodParameter(methodName, value[i], defaultExtName) 25 // 以 LoadBalance 介面的 select 方法為例,最終生成的程式碼如下: 26 // url.getMethodParameter(methodName, "loadbalance", "random") 27 getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", 28 value[i], defaultExtName); 29 } else { 30 // 生成的程式碼功能等價於下面的程式碼: 31 // url.getParameter(value[i], defaultExtName) 32 getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], 33 defaultExtName); 34 } 35 } else { 36 // 生成的程式碼功能等價於下面的程式碼: 37 // ( url.getProtocol() == null ? defaultExtName : url.getProtocol() ) 38 getNameCode = String.format( 39 "( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName); 40 // 預設副檔名為空 41 } 42 } else { 43 if (!"protocol".equals(value[i])) { 44 if (hasInvocation) { 45 // 生成程式碼格式同上 46 getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", 47 value[i], defaultExtName); 48 } else { 49 // 生成的程式碼功能等價於下面的程式碼: 50 // url.getParameter(value[i]) 51 getNameCode = String.format("url.getParameter(\"%s\")", value[i]); 52 } 53 } else { 54 // 生成從 url 中獲取協議的程式碼,比如 "dubbo" 55 getNameCode = "url.getProtocol()"; 56 } 57 } 58 } else { 59 if (!"protocol".equals(value[i])) { 60 if (hasInvocation) { 61 // 生成程式碼格式同上 62 getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", 63 value[i], defaultExtName); 64 } else { 65 // 生成的程式碼功能等價於下面的程式碼: 66 // url.getParameter(value[i], getNameCode) 67 // 以 Transporter 介面的 connect 方法為例,最終生成的程式碼如下: 68 // url.getParameter("client", url.getParameter("transporter", "netty")) 69 getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode); 70 } 71 } else { 72 // 生成的程式碼功能等價於下面的程式碼: 73 // url.getProtocol() == null ? getNameCode : url.getProtocol() 74 // 以 Protocol 介面的 connect 方法為例,最終生成的程式碼如下: 75 // url.getProtocol() == null ? "dubbo" : url.getProtocol() 76 getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", 77 getNameCode); 78 } 79 } 80 } 81 ...暫時省略....
第8行獲取預設副檔名。來自於@SPI註解的value值。
第13行開始迴圈value,此處的value是@Adaptive註解的內容,前面有過分析,如果@Adaptive沒有設定value,則通過type名稱解析出value。
上面的程式碼分支比較多,但是主要是集中在protocol、hasInvocation的判斷上。
首先看hasInvocation,如果hasInvocation不為空,我們看到生成的程式碼是“url.getMethodParameter(methodName...”,這個methodName也是前面判斷hasInvocation時獲取到的。這等於在從URL中獲取值的時候,加上了methodName。如果hasInvocation為空,此時的分支生成的程式碼,直接是“url.getParameter(.."。
第二個是判斷protocol的分支,由於URL類中有單獨的protocol變數,所以 如果value值為protocol,此時從URL中取值,可以直接呼叫url.getProtocol(),不需要通過URL的getParameter方法。
上面的程式碼是從URL中獲取擴充套件點key的主要邏輯,分支比較多,但是很多重複的程式碼,也進行了比較詳細的註釋。
我們接著往下看,從URL中拿到擴充套件點的key後的程式碼
1 code.append("\nString extName = ").append(getNameCode).append(";"); 2 // check extName == null? 3 String s = String.format("\nif(extName == null) " 4 + "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");", 5 type.getName(), Arrays.toString(value)); 6 code.append(s); 7 // 生成擴充套件獲取程式碼,格式如下: 8 // type 全限定名 extension = (type全限定名)ExtensionLoader全限定名 9 // .getExtensionLoader(type全限定名.class).getExtension(extName); 10 // Tips: 格式化字串中的 %<s 表示使用前一個轉換符所描述的引數,即 type 全限定名 11 s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);", 12 type.getName(), ExtensionLoader.class.getSimpleName(), type.getName()); 13 code.append(s); 14 15 // return statement 16 // 如果方法返回值型別非 void,則生成 return 語句。 17 if (!rt.equals(void.class)) { 18 code.append("\nreturn "); 19 } 20 21 s = String.format("extension.%s(", method.getName()); 22 code.append(s); 23 for (int i = 0; i < pts.length; i++) { 24 if (i != 0) 25 code.append(", "); 26 code.append("arg").append(i); 27 } 28 code.append(");");
這段程式碼就比較簡單明瞭了,核心在第11行,強制轉換為擴充套件點type型別,通過ExtensionLoader的getExtensionLoader獲取type介面對應的ExtensionLoader例項。
現在已經拿到的擴充套件點實現的key,只要呼叫ExtensionLoader例項的getExtension()方法,即可返回需要呼叫的擴充套件點實現。
我們分析的主線是按擴充套件點的一個方法進行,每個被@Adaptive修飾的方法,生成的邏輯都是一樣的,主要的邏輯是:
1、根據@Adaptive註解的value,或是擴充套件點type的名稱生成從URL獲取擴充套件點實現類key的關鍵字
2、根據第一步獲取的關鍵字,從URL中獲取要呼叫的擴充套件點實現類的key
3、獲取到擴充套件點實現類對應的key,呼叫ExtensionLoader例項的getExtension()方法,即可拿到對應的擴充套件點實現
4、方法的執行是呼叫擴充套件點實現類的目標方法。
至此新類的字串已經生成了,我們回到createAdaptiveExtensionClass方法
1 private Class<?> createAdaptiveExtensionClass() { 2 // 建立自適應擴充套件程式碼 字串 3 String code = createAdaptiveExtensionClassCode(); 4 ClassLoader classLoader = findClassLoader(); 5 // 獲取編譯器實現類 6 com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader 7 .getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); 8 // 編譯程式碼,獲取自適應擴充套件類的Class 9 return compiler.compile(code, classLoader); 10 }
第3行就是我們前面分析的獲取新類字串的方法,拿到code之後,再獲取類載入器,獲取編輯器,執行編譯。返回的就是自適應擴充套件類的Class物件。
通過此方法,再往上返回就是自適應擴充套件類的物件,以及快取物件等邏輯。自適應擴充套件的獲取基本就結束了。
四、總結
通過上面的分析,我們基本瞭解的自適應擴充套件點的實現邏輯,難點就是@Adaptive註解方法時,生成新類的字串之處。別的邏輯還算清晰。如果在讀到此處有困惑,請評論留言,我會進行詳細解