1. 程式人生 > >dubbo原始碼(章節二) -- 核心探索之SPI

dubbo原始碼(章節二) -- 核心探索之SPI

dubbo為什麼不採用jdk的spi?

  • jdk標準的spi會一次性例項化擴充套件點的所有實現,如果有擴充套件實現初始化很耗時,或者有的擴充套件實現沒有使用到也會被載入,會造成資源浪費。
  • dubbo增加了對擴充套件點的ioc和aop的支援,一個擴充套件點可以直接setter注入其他的擴充套件點。

dubbo spi的一些約定:

spi檔案的儲存路徑:

META-INF/dubbo/internal/com.xxx.Protocol

其中com.xxx.Protocal代表檔名,即介面的全路徑名。

spi的檔案格式定義為:

xxx=com.foo.XxxProtocol
yyy=com.foo.YyyProtocol

這麼定義的原因是:如果擴充套件實現中的靜態屬性或方法引用了某些第三方庫,而該庫又依賴缺失的話,就會導致這個擴充套件實現初始化失敗。在這種情況下,dubbo無法定位失敗的擴充套件id,所以無法精確定位異常資訊。

dubbo spi的目的:

獲取一個擴充套件實現類的物件。

ExtensionLoader<T> getExtensionLoader(Class<T> type)

為了獲取到擴充套件實現類的物件,需要先為該介面獲取一個extensionLoader,快取起來,之後通過這個extensionLoader獲取到對應的extension。 

由extensionLoader獲取對應extension主要有兩種方式:

/**
 * Find the extension with the given name. */
getExtension(String name)

通過給定的name獲取物件。

/**
 * Get activate extensions.
 */
getAdaptiveExtension()

獲取一個擴充套件裝飾類物件,dubbo的擴充套件介面有個規則,它所有的實現類中,要麼有且僅有一個類被@Adaptive標註,getAdaptiveExtension()返回的就是這個類物件,要麼所有的實現類都沒有@Adaptive註解,此時,dubbo就動態建立一個代理類並由getAdaptiveExtension()返回。這塊後面詳細談論。

下面先討論extensionLoader的獲取

先從dubbo的第一行程式碼開始:

com.alibaba.dubbo.container.Main.main(args);

從這裡的main方法進入,就來到dubbo的Main class,這裡定義了一個靜態初始化的變數loader,這是dubbo的第一個擴充套件點,我們從這裡開始跟程式碼,

private static final ExtensionLoader<Container> loader =     ExtensionLoader.getExtensionLoader(Container.class);

進入getExtensionLoader內部,

 1 private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS =      new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
 2 
 3 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
 4   ......
 5   ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
 6   if (loader == null) {
 7     EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
 8     loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
 9   }
10   return loader;
11 }

這個方法的入參為Class<T> type,標誌了將要獲取到的extensionLoader的擴充套件介面的型別,此時實際的傳入引數為Container.class。

同時大家注意,我們前面說過,為擴充套件介面建立extensionLoader時,所建立的extensionLoader會被快取起來,所以我們這裡看到一個concurrentHashMap被申明用做快取。

上述程式碼顯示快取中查詢為null時,會建立一個extensionLoader<T>,我們繼續跟蹤new ExtensionLoader<T>(type),從程式碼第7行進入,

1 private ExtensionLoader(Class<?> type) {
2   this.type = type;
3   objectFactory = (type == ExtensionFactory.class ? null :       ExtensionLoader.getExtensionLoader(ExtensionFactory.class).        getAdaptiveExtension());
4 }

首先為擴充套件介面的型別type賦值,這裡是Container.class,大家注意一個細節,前面getExtensionLoader方法的入參是Class<T>,而這裡卻是Class<?>,由泛型變成了萬用字元,稍後解釋原因。

第三行為objectFactory賦值,暫時先不管objectFactory的作用,我們先看主幹邏輯,因為這裡type是Container.class,三元運算子進入後半部分,再次呼叫getExtensionLoader,並傳入引數ExtensionFactory.class。

繼續跟蹤程式碼,在getExtensionLoader內部它又會去查詢快取,因為這裡還是不存在ExtensionFactory.class的key,所以繼續進入new ExtensionLoader<T>(type)的邏輯,這次的傳入引數是ExtensionFactory.class,所以objectFactory被賦值為null。

這裡就可以看出來了,兩次呼叫ExtensionLoader的構造方法,入參分別為Container.class和ExtensionFactory.class,所以構造方法的入參使用萬用字元,同時concurrentHashMap作為快取,它的key也是Class<?>。

ok,到目前為止,我們總結下呼叫鏈:

ExtensionLoader.getExtensionLoader(Container.class);
    -->this.type = type;
    -->objectFactory = ExtensionLoader.getExtensionLoader(ExtensionFactory.class).      getAdaptiveExtension();
        -->ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
            -->this.type = type;
            -->objectFactory = null;

快取中應該有兩項記錄了,

{
  "key": "Container.class",
  "value": "......"},
{
  "key": "ExtensionFactory.class",
  "value": "......"}

總結下以上程式碼,每一個ExtensionLoader都包含有兩個屬性type,objectFactory:

  • Class<T> type,初始化時要得到的介面名。
  • ExtensionFactory objectFactory,初始化一個AdaptiveExtensionFactory。objectFactory的作用是:為dubbo的Ioc提供所有物件,這個的實現原理還是相當複雜的,之後單開一篇來說。

ok,現在我們獲取到了ExtensionLoader,接下來就是獲取Extension了。

下面討論getAdaptiveExtension()

回憶前面在建立ExtensionLoader<Container>時,要初始化它的objectFactory:

objectFactory = ExtensionLoader.getExtensionLoader(ExtensionFactory.class)  .getAdaptiveExtension();

這裡就是先獲取ExtensionLoader<ExtensionFactory>,之後呼叫了getAdaptiveExtension()。所以我們跟蹤這個方法:

1 public T getAdaptiveExtension() {
2   Object instance = cachedAdaptiveInstance.get();
3   if (instance == null) {4     instance = createAdaptiveExtension();
5     cachedAdaptiveInstance.set(instance);6   }
7   return (T) instance;
8 }

為了節約篇幅,非主幹邏輯的程式碼這裡做了省略,可以看到,這個方法主要是為了給變數cachedAdaptiveInstance賦值。繼續跟蹤程式碼第四行:

1 private T createAdaptiveExtension() {2   return injectExtension((T) getAdaptiveExtensionClass().newInstance());3 }

這裡呼叫了getAdaptiveExtensionClass(),先獲取擴充套件的類物件,再例項化出具體的擴充套件物件,我們進入getAdaptiveExtensionClass():

1 private Class<?> getAdaptiveExtensionClass() {
2   getExtensionClasses();
3   if (cachedAdaptiveClass != null) {
4     return cachedAdaptiveClass;
5   }
6   return cachedAdaptiveClass = createAdaptiveExtensionClass();
7 }

這裡涉及到一個全域性變數cachedAdaptiveClass,這個變數很重要,留意一下,稍後馬上就會說到,這裡我們先看getExtensionClasses()做了什麼:

private Map<String, Class<?>> getExtensionClasses() {  Map<String, Class<?>> classes = cachedClasses.get();  if(classes == null){    classes = loadExtensionClasses();    cachedClasses.set(classes);  }  return classes;
}

省約了非主幹邏輯,這裡其實就是載入所有的ExtensionClasses,也就是該擴充套件介面type的所有實現類,繼續跟進:

private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

private Map<String, Class<?>> loadExtensionClasses() {
  ......
  Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
  loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
  loadDirectory(extensionClasses, DUBBO_DIRECTORY);
  loadDirectory(extensionClasses, SERVICES_DIRECTORY);
  return extensionClasses;
}

這裡依次載入三個目錄,由於我們知道spi的儲存路徑就是META-INF/dubbo/internal/,所有我們這裡暫時只關注第一個loadDirectory即可,

 1 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
 2   String fileName = dir + type.getName(); 3   Enumeration<java.net.URL> urls;
 4   ClassLoader classLoader = findClassLoader(); 5   urls = classLoader.getResources(fileName);        6   while (urls.hasMoreElements()) {
 7     java.net.URL resourceURL = urls.nextElement();
 8     loadResource(extensionClasses, classLoader, resourceURL);
 9   }10 }

這裡傳入的dir就是dubbo spi的儲存路徑META-INF/dubbo/internal/,type.getName()獲得擴充套件介面的類路徑,兩者拼接就得到一個dubbo spi的完整路徑,這裡的話就是:META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ExtensionFactory。

獲取該路徑下的所有url資源,逐個處理,我們跟蹤方法loadResource(......),

 1 private void loadResource(......) { 2   ...... 3   String line;
 4   while ((line = reader.readLine()) != null) { 5     int i = line.indexOf('='); 6     String name = line.substring(0, i).trim();       7     line = line.substring(i+1).trim(); 8     loadClass(......, Class.forName(line, true, classLoader), name); 9   }10 }

這個類的作用就是通過傳入的url,讀取檔案,並逐行處理,前面說過dubbo spi的檔案格式為xxx=com.foo.XXX,所以這裡解析字串就可以拿到擴充套件實現類的類名及對應的類路徑,下一步就是載入這些實現類了,再這之前我們不妨先看看ExtensionFactory的擴充套件實現都有哪些,手動開啟META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ExtensionFactory檔案,檢視裡面的內容,

adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactory

我們看到有兩個實現,分別是AdaptiveExtensionFactory、SpiExtensionFactory,下面就將分別載入它們,

 1 private void loadClass(......, Class<?> clazz, String name) { 2   if (clazz.isAnnotationPresent(Adaptive.class)) {
 3     if (cachedAdaptiveClass == null) {
 4       cachedAdaptiveClass = clazz;
 5     } else if (!cachedAdaptiveClass.equals(clazz)) {
 6       throw new IllegalStateException("More than 1 adaptive class found: "......) 7     }
 8   } else if (isWrapperClass(clazz)) {
 9     Set<Class<?>> wrappers = cachedWrapperClasses;
10     if (wrappers == null) {
11       cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
12       wrappers = cachedWrapperClasses;
13     }
14     wrappers.add(clazz);
15   } else {
16     clazz.getConstructor();17     ......18     cachedActivates.put(name, activate);19     cachedNames.put(clazz, name);20     extensionClasses.put(name, clazz);21   }
22 }
1 private boolean isWrapperClass(Class<?> clazz) {
2   try {
3     clazz.getConstructor(type);
4     return true;
5   } catch (NoSuchMethodException e) {
6     return false;
7   }
8 }

程式第二行判斷如果當前類擁有@Adaotive註解,則為全域性變數cachedAdaptiveClass賦值,否則,程式第八行判斷如果當前類不包含@Adpative註解,且當前類的構造器方法包含目標介面(type)型別,則當前類被加入cachedWrapperClasses快取,否則,如果當前類即不被@Adaptive註解,也沒有type型別的構造器,它最終會被加入到extensionClasses中,extensionClasses最終賦值給了cachedClasses。

注意程式碼第五行,cachedAdaptiveClass是Class<?>型別的變數,也就是說,只能是一個值,那麼當我們逐行載入spi配置檔案裡的類時,如果有兩個類都標註了@Adaptive註解呢?第二個被標註的類會直接拋異常,因為此時cachedAdaptiveClass已經被賦值了第一個類的型別,它當然不會equals第二個類了,由此就證明了我們開篇提到的dubbo spi的規則,它的所有實現類中,最多有一個被@Adaptive註解。

ok,到了這裡,載入擴充套件classes的過程就結束了,我們回到getExtensionClasses()的呼叫處,在方法getAdaptiveExtensionClass()中,完成了classes的載入之後,接下來就判斷全域性變數cachedAdaptiveClass的值,如果不為null,則表明該變數在上述類載入過程中被賦值了,我們前面描述了,這個值就只能是一個帶有@Adaptive註解的擴充套件類。如果該變數仍然為null,則表明這個擴充套件介面沒有一個實現類帶有@Adaptive註解,大家回憶一下,前面我們說過dubbo spi的規則,它要麼只有一個實現類被@Adaptive註解,要麼就沒有,如果它沒有一個實現類被@Adaptive註解,那麼就動態建立一個代理類,這句話就體現在這裡了,顯而易見,方法createAdaptiveExtensionClass()就將被用來完成這件事。

1 private Class<?> createAdaptiveExtensionClass() {
2   String code = createAdaptiveExtensionClassCode();
3   ClassLoader classLoader = findClassLoader();
4   Compiler compiler = ExtensionLoader.getExtensionLoader(Compiler.class)5     .getAdaptiveExtension();
6   return compiler.compile(code, classLoader);
7 }

這個過程分四步來做,首先在程式第二行,將動態生成一個代理類,這個的實現程式碼非常繁瑣,這裡不貼出來了,其原理就是通過一個Adpative類的模板來生成代理類,我總結下模板給大家參考,

package <擴充套件點介面所在包>public class <擴充套件點介面名>$Adaptive implements <擴充套件點介面>{
  public <有@Adaptive註解的介面方法>(引數){    //這裡生成代理物件,執行方法  }
}

也就是說,代理類只會生成介面中被@Adaptive註解了的方法,如果試圖在代理類中呼叫介面中沒有標註@Adaptive的方法,程式會丟擲異常。

因為這裡當前的type是ExtensionFactory,這個介面是擁有一個被@Adaptive註解了的類的,所以我們debug到這裡會直接獲得AdaptiveExtensionFactory,不會觸發動態生成代理類的過程,為了瞭解動態代理類到底長什麼樣子,這裡我給大家舉個例子,Protocol是dubbo的一個spi介面,它的所有實現類沒有一個被@Adaptive註解,所以如果我們執行下面這句程式碼,

ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

它在通過spi配置檔案的類載入中得不到一個Adaptive的實現類,所以程式碼最終會進入createAdaptiveExtensionClass()方法中,我們通過debug拿到String code的值,如下,

 1 package com.alibaba.dubbo.rpc;
 2 import com.alibaba.dubbo.common.extension.ExtensionLoader; 3
 4 public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
 5   public void destroy() { 6     throw new Exception("method destroy() of Protocol is not adaptive method!");
 7   } 8
 9   public int getDefaultPort() {10     throw new Exception("... not adaptive method!");
11   }12
13   public Exporter export(Invoker arg0) throws RpcException { 14     ......15     Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class)16       .getExtension(extName);
17     return extension.export(arg0);
18   }19
20   public Invoker refer(Class arg0, URL arg1) throws RpcException {
21     ......22     Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class)23       .getExtension(extName);
24     return extension.refer(arg0, arg1);
25   }
26 }

我們對比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 }

可以看到,方法export和refer被標註了@Adaptive註解,所以在代理類中生成了代理方法,為什麼說是代理方法呢,因為在方法內部又生成了一個Protocol物件,通過這個物件完成了方法呼叫,物件extension就是一個不折不扣的代理物件。另外,在介面中方法getDefaultPort()和方法destroy()沒有被標註@Adaptive註解,所以在代理類中它們沒有被實現。

生成了代理類之後,下一步,就是動態編譯這個代理類,關於動態編譯的內容,之後單獨開一篇來討論。另外這裡從程式碼中可以看到,compiler也是由spi獲得的,實際上,dubbo所有擴充套件物件的獲取都是通過spi完成的,故而我們也說spi是dubbo核心的靈魂之所在。

ok,不論是通過配置檔案載入,還是通過動態編譯生成代理類,到這裡為止,getAdaptiveExtensionClass()方法就執行完了,我們終於獲得了擴充套件類的類物件,但這還不是擴充套件物件,繼續回到方法getAdaptiveExtensionClass()的呼叫處,

1 private T createAdaptiveExtension() {2   return injectExtension((T) getAdaptiveExtensionClass().newInstance());3 }

這裡我們拿剛剛返回的類物件new了一個Instance出來,之後就作為引數被傳入injectExtension()方法中了,這個方法會進入dubbo的Ioc,控制反轉,實現擴充套件物件的動態注入,Ioc的相關內容下一篇文章再做討論。

dubbo的adaptive spi,主要邏輯到這裡就梳理完成了,spi是dubbo核心的靈魂,同時它的實現原理也是頗覆雜的,這裡提到的幾段程式碼需要結合debug反覆跟蹤,不斷琢磨。