dubbo的SPI應用與原理

java06051515發表於2020-03-03

dubbo

SPI(Service Provider Interface)

  • 本質是 將介面實現類的全限定名配置在檔案中,並由服務載入器讀取配置檔案,載入實現類。這樣可以在執行時,動態為介面替換實現類。
  • 在Java中SPI是被用來設計給服務提供商做外掛使用的。基於策略模式 來實現動態載入的機制 。我們在程式只定義一個介面,具體的實現交個不同的服務提供者;在程式啟動的時候,讀取配置檔案,由配置確定要呼叫哪一個實現;
  • 透過 SPI 機制為我們的程式提供擴充功能,在dubbo中,基於 SPI,我們可以很容易的對 Dubbo 進行擴充。例如dubbo當中的protocol,LoadBalance等都是透過SPI機制擴充套件。

想要學習 Dubbo 的原始碼,SPI 機制務必弄懂。接下來,我們瞭解下JAVA SPI與dubbo SPI的用法,再分析DUBBO SPI的原始碼,本文的dubbo原始碼是基於2.7.5版本。

JAVA 原生SPI 示例

  • 先簡單介紹JAVA SPI的應用。首先,我們定義一個Car介面
public interface Car {    String getBrand();
}
  • 定義該介面的兩個實現類。
public class BM implements Car {    public String getBrand() {
        System.out.println("BM car");        return "BM";
    }
}public class Benz implements Car {    public String getBrand() {
        System.out.println("benz car");        return "Benz";
    }
}
  • 再在resources下建立META-INF/services 資料夾,並建立一個檔案,檔名稱為Car介面的全限定名com.dubbo.dp.spi.Car。內容為介面實現類的全限定類名。
    com.dubbo.dp.spi.Benzcom.dubbo.dp.spi.BM
  • 使用如下,呼叫Car介面在配置檔案中的所有實現類。
    public class JavaSPITest {  @Test
      public void sayHello() throws Exception {
          ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
          serviceLoader.forEach(car -> System.out.println(car.getBrand()));
      }
    }
    JAVA SPI實現了介面的定義與具體業務實現解耦,應用程式可以根據實際業務情況啟用或替換具體元件。

舉例:JAVA的java.sql包中就定義一個介面Driver,各個服務提供商實現該介面。當我們需要使用某個資料庫時就匯入相應的jar包。

缺點

  • 不能按需載入。Java SPI在載入擴充套件點的時候,會一次性載入所有可用的擴充套件點,很多是不需要的,會浪費系統資源;
  • 獲取某個實現類的方式不夠靈活,只能透過 Iterator 形式獲取,不能根據某個引數來獲取對應的實現類。
  • 不支援AOP與依賴注入。
  • JAVA SPI可能會丟失載入擴充套件點異常資訊,導致追蹤問題很困難;

dubbo SPI示例

  • dubbo重新實現了一套功能更強的 SPI 機制, 支援了AOP與依賴注入,並且 利用快取提高載入實現類的效能,同時 支援實現類的靈活獲取,文中接下來將講述SPI的應用與原理。

Dubbo的SPI介面都會使用@SPI註解標識,該註解的主要作用就是標記這個介面是一個SPI介面。原始碼如下:

@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI {    /**
     * default extension name
     * 設定預設擴充類
     */
    String value() default "";
}

該註解只作用在介面上,value用來設定預設擴充類

  • 首先講解下dubbo SPI的使用。在上述Car介面加上@SPI註解,它的實現類暫時不變,配置檔案的路徑與檔名也暫時不變,檔案內容調整如下:
    @SPIpublic interface Car {  String getBrand();
    }
    benz=com.dubbo.dp.spi.Benzbm=com.dubbo.dp.spi.BM
    配置檔案透過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。使用如下:
public class JavaSPITest {    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);        //按需獲取實現類物件
        Car car = carExtensionLoader.getExtension("benz");
        System.out.println(car.getBrand());
    }
}

輸出結果為

benz car
Benz

Dubbo SPI 原始碼分析

在dubbo SPI示例方法中,我們首先透過 ExtensionLoader的  getExtensionLoader 方法獲取一個介面的  ExtensionLoader 例項,然後再透過  ExtensionLoader 的  getExtension 方法獲取擴充類物件,原始碼如下,首先是 getExtensionLoader 方法:

    /**
     * 擴充套件類載入器快取,就是擴充套件點ExtendsLoader例項快取; key=擴充套件介面 value=擴充套件類載入器
     */
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {        //校驗傳進的type類是否為空
        if (type == null) {            throw new IllegalArgumentException("Extension type == null");
        }        //校驗傳進的type類是否為介面
        if (!type.isInterface()) {            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }        //校驗傳進的type類是否有@SPI註解
        if (!withExtensionAnnotation(type)) {            throw new IllegalArgumentException("Extension type (" + type +                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }        //從ExtensionLoader快取中查詢是否已經存在對應型別的ExtensionLoader例項
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);        if (loader == null) {            //沒有就new一個ExtensionLoader例項,並存入本地快取
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }        return loader;
    }

getExtensionLoader會對傳進的介面進行校驗,其中包括是否有 @SPI註解校驗,這也是在介面上需加 @SPI的原因。然後從 EXTENSION_LOADERS快取中獲取該介面型別的 ExtensionLoader,如果獲取不到,則建立一個該介面型別的 ExtensionLoader放入到快取中,並返回該 ExtensionLoader

注意這裡建立 ExtensionLoader物件的構造方法如下:ExtensionLoader.getExtensionLoader獲取ExtensionFactory介面的擴充類,再透過 getAdaptiveExtension從擴充類中獲取目標擴充類。它會設定該介面對應的  objectFactory常量為 AdaptiveExtensionFactory。因為 AdaptiveExtensionFactory類上加了@Adaptive註解,為什麼是 AdaptiveExtensionFactory原因在之後的文章會解釋,且 objectFactory也會在後面用到。

    private ExtensionLoader(Class<?> type) {        this.type = type;        //type通常不為ExtensionFactory類,則objectFactory為ExtensionFactory介面的預設擴充套件類AdaptiveExtensionFactory
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

當透過 ExtensionLoader.getExtensionLoader取到介面的載入器Loader之後,在透過 getExtension方法獲取需要擴充類物件。該方法的整個執行流程如下圖所示
獲取擴充類例項順序圖

參照執行流程圖,擴充類物件的獲取原始碼如下:

    /**
    * 擴充套件點例項快取 key=擴充套件點名稱,value=擴充套件例項的Holder例項
    */
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();    /**
     * 獲取介面擴充類例項
     * 1.檢查快取中是否存在
     * 2.建立並返回擴充類例項
     * @param name  需要獲取的配置檔案中擴充類的key
     * @return
     */
 public T getExtension(String name) {        if (StringUtils.isEmpty(name)) {            throw new IllegalArgumentException("Extension name == null");
        }        if ("true".equals(name)) {            // 獲取預設的擴充實現類,即@SPI註解上的預設實現類, 如@SPI("benz")
            return getDefaultExtension();
        }        // Holder,顧名思義,用於持有目標物件,從快取中拿,沒有則建立
        final Holder<Object> holder = getOrCreateHolder(name);
        Object instance = holder.get();        // 雙重檢查
        if (instance == null) {            synchronized (holder) {
                instance = holder.get();                if (instance == null) {                    // 建立擴充例項
                    instance = createExtension(name);                    // 設定例項到 holder 中
                    holder.set(instance);
                }
            }
        }        return (T) instance;
    }    /**
     * 獲取或者建立一個Holder物件
     */
    private Holder<Object> getOrCreateHolder(String name) {        // 首先透過副檔名從擴充套件例項快取中獲取Holder物件
        Holder<Object> holder = cachedInstances.get(name);        if (holder == null) {            //如果沒有獲取到就new一個空的Holder例項存入快取
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }        return holder;
    }

上面程式碼的邏輯比較簡單,首先檢查快取,快取未命中則建立擴充物件。dubbo中包含了大量的擴充套件點快取。這個就是典型的使用空間換時間的做法。也是Dubbo效能強勁的原因之一,包括

  1. 擴充套件點Class快取 ,Dubbo SPI在獲取擴充套件點時,會優先從快取中讀取,如果快取中不存在則載入配置檔案,根據配置將Class快取到記憶體中,並不會直接初始化。
  2. 擴充套件點例項快取 ,Dubbo不僅會快取Class,還會快取Class的例項。每次取例項的時候會優先從快取中取,取不到則從配置中載入,例項化並快取到記憶體中。

下面我們來看一下建立擴充物件的過程

    /**
     * 擴充套件例項存入記憶體中快取起來; key=擴充套件類 ; value=擴充套件類例項
     */
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();    /**
     * 建立擴充類例項,包含如下步驟
     * 1. 透過 getExtensionClasses 獲取所有的擴充類,從配置檔案載入獲取擴充類的map對映
     * 2. 透過反射建立擴充物件
     * 3. 向擴充物件中注入依賴(IOC)
     * 4. 將擴充物件包裹在相應的 Wrapper 物件中(AOP)
     * @param name 需要獲取的配置檔案中擴充類的key
     * @return 擴充類例項
     */
    private T createExtension(String name) {        // 從配置檔案中載入所有的擴充類,可得到“配置項名稱”到“配置類”的map,再根據擴充項名稱從map中取出相應的擴充類即可
        Class<?> clazz = getExtensionClasses().get(name);        if (clazz == null) {            throw findException(name);
        }        try {            //從擴充套件點快取中獲取對應例項物件
            T instance = (T) EXTENSION_INSTANCES.get(clazz);            if (instance == null) {                // //如果快取中不存在此類的擴充套件點,就透過反射建立例項,並存入快取
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());                //然後從快取中獲取對應例項
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }            // 向例項中注入依賴,透過setter方法自動注入對應的屬性例項
            injectExtension(instance);            //從快取中取出所有的包裝類,形成包裝鏈
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;            if (CollectionUtils.isNotEmpty(wrapperClasses)) {                // 迴圈建立 Wrapper 例項,形成Wrapper包裝鏈
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }            //初始化例項並返回
            initExtension(instance);            return instance;
        } catch (Throwable t) {            throw new IllegalStateException(".....");
        }
    }

建立擴充類物件步驟分別為:

  1. 透過 getExtensionClasses 從配置檔案中載入所有的擴充類,再透過名稱獲取目標擴充類
  2. 透過反射建立擴充物件
  3. 向擴充物件中注入依賴
  4. 將擴充物件包裹在相應的 Wrapper 物件中

第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現。我們先重點分析getExtensionClasses 方法的邏輯 getExtensionClasses 方法的邏輯。

從配置檔案中載入所有的擴充類

  • 在透過name獲取擴充類之前,首先需要根據配置檔案解析出擴充項名稱與擴充類的對映map,之後再根據擴充項名稱從map中取出相應的擴充類即可。 getExtensionClasses 方法原始碼如下

      /**
       * 擴充套件點Class快取 key=副檔名 ,value=對應的class物件
       */
      private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();  /**
       * 解析配置檔案中介面的擴充項名稱與擴充類的對映表map
       * @return
       */
      private Map<String, Class<?>> getExtensionClasses() {      // 從快取中獲取已載入的擴充點class
          Map<String, Class<?>> classes = cachedClasses.get();      //雙重檢查
          if (classes == null) {          synchronized (cachedClasses) {
                  classes = cachedClasses.get();              if (classes == null) {                  // 載入擴充類
                      classes = loadExtensionClasses();
                      cachedClasses.set(classes);
                  }
              }
          }      return classes;
      }

    這裡也是先檢查快取,若快取未命中,則透過 loadExtensionClasses 載入擴充類,快取避免了多次讀取配置檔案的耗時。下面分析 loadExtensionClasses方法載入配置檔案的邏輯

      /**
       * 三個dubbo SPI預設掃描的路徑
       */
      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() {      //獲取並快取介面的@SPI註解上的預設實現類,@SPI("value")中的value
          cacheDefaultExtensionName();
          Map<String, Class<?>> extensionClasses = new HashMap<>();      // 載入指定資料夾下的配置檔案,常量包含META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/三個資料夾
          loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);      //相容歷史版本
          loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);
          loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
          loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
          loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
          loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));      return extensionClasses;
      }

    loadExtensionClasses 方法總共做了兩件事情。首先該方法呼叫 cacheDefaultExtensionName對 SPI 註解進行解析,獲取並快取介面的 @SPI註解上的預設擴充類在 cachedDefaultName。再呼叫  loadDirectory 方法載入指定資料夾配置檔案。

SPI 註解解析過程比較簡單,原始碼如下。只允許一個預設擴充類。

    private void cacheDefaultExtensionName() {        // 獲取 SPI 註解,這裡的 type 變數是在呼叫 getExtensionLoader 方法時傳入,代表介面類
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);        if (defaultAnnotation == null) {            return;
        }
        String value = defaultAnnotation.value();        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);            // 檢測 SPI 註解內容是否合法(至多一個預設實現類),不合法則丟擲異常
            if (names.length > 1) {                throw new IllegalStateException("...");
            }            // 設定預設擴充類名稱
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

從原始碼中可以看出 loadExtensionClasses方法載入配置檔案的路徑有3個,分別為 META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/三個資料夾。方法原始碼如下:

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
        loadDirectory(extensionClasses, dir, type, false);
    }    /**
     * 載入配置檔案內容
     * @param extensionClasses 擴充類map
     * @param dir 資料夾路徑
     * @param type 介面名稱
     * @param extensionLoaderClassLoaderFirst 是否先載入ExtensionLoader的ClassLoader
     */
    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {        // fileName = 資料夾路徑 + type 全限定名
        String fileName = dir + type;        try {
            Enumeration<java.net.URL> urls = null;            //獲取當前執行緒的類載入器
            ClassLoader classLoader = findClassLoader();            // try to load from ExtensionLoader's ClassLoader first
            if (extensionLoaderClassLoaderFirst) {                //獲取載入ExtensionLoader.class這個類的類載入器
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();                //如果extensionLoaderClassLoaderFirst=true時,且這兩個類載入器不同,就優先使用 extensionLoaderClassLoader
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }            // 根據檔名載入所有的同名檔案
            if(urls == null || !urls.hasMoreElements()) {                if (classLoader != null) {
                    urls = classLoader.getResources(fileName);
                } else {
                    urls = ClassLoader.getSystemResources(fileName);
                }
            }            if (urls != null) {                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();                    // 解析並載入配置檔案中配置的實現類到extensionClasses中去
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("").", t);
        }
    }

首先找到資料夾下的配置檔案,檔名需為介面全限定名。利用類載入器獲取檔案資源連結,再解析配置檔案中配置的實現類新增到 extensionClasses中。我們繼續看loadResource是如何載入資源的。

    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {        try {            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
                String line;                // 按行讀取配置內容
                while ((line = reader.readLine()) != null) {                    final int ci = line.indexOf('#');                    if (ci >= 0) {                        // 擷取 # 之前的字串,# 之後的內容為註釋,需要忽略
                        line = line.substring(0, ci);
                    }
                    line = line.trim();                    if (line.length() > 0) {                        try {
                            String name = null;                            int i = line.indexOf('=');                            if (i > 0) {                                // 以等於號 = 為界,擷取鍵與值
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            }                            if (line.length() > 0) {                                // 透過反射載入類,並透過 loadClass 方法對類進行快取
                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                            }
                        } catch (Throwable t) {
                            .....
                        }
                    }
                }
            }
        } catch (Throwable t) {
            logger.error(....);
        }
    }

loadResource 方法用於讀取和解析配置檔案,按行讀取配置檔案,每行以等於號 = 為界,擷取鍵與值,並透過反射載入類,最後透過 loadClass方法載入擴充套件點實現類的class到map中,並對載入到的class進行分類快取。 loadClass方法實現如下

    /**
     * 載入擴充套件點實現類的class到map中,並對載入到的class進行分類快取
     * 比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等
     * @param extensionClasses 裝載配置檔案類的容器
     * @param resourceURL 配置檔案資源URL
     * @param clazz 擴充套件點實現類的class
     * @param name  擴充套件點實現類的名稱,配置檔案一行中的key
     * @throws NoSuchMethodException
     */
    private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {        //判斷配置的實現類是否是實現了type介面
        if (!type.isAssignableFrom(clazz)) {            throw new IllegalStateException("...");
        }        //根據配置中實現類的型別來分類快取起來
        // 檢測目標類上是否有 Adaptive 註解,表示這個類就是一個自適應實現類,快取到cachedAdaptiveClass
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz);        // 檢測 clazz 是否是 Wrapper 型別,判斷依據是是否有引數為該介面類的構造方法,快取到cachedWrapperClasses
        } else if (isWrapperClass(clazz)) {
            cacheWrapperClass(clazz);
        } else {            // 檢測 clazz 是否有預設的構造方法,如果沒有,則丟擲異常
            clazz.getConstructor();            // 如果配置檔案中key的name 為空,則嘗試從Extension註解中獲取 name,或使用小寫的類名作為name。
            // 已經棄用,就不在討論這種方式
            if (StringUtils.isEmpty(name)) {
                name = findAnnotationName(clazz);                if (name.length() == 0) {                    throw new IllegalStateException("...");
                }
            }            //使用逗號將name分割為字串陣列
            String[] names = NAME_SEPARATOR.split(name);            if (ArrayUtils.isNotEmpty(names)) {                //如果擴充套件點配置的實現類使用了@Activate註解,就將對應的註解資訊快取起來
                cacheActivateClass(clazz, names[0]);                for (String n : names) {                    //快取擴充套件點實現類class和擴充套件點名稱的對應關係
                    cacheName(clazz, n);                    //最後將class存入extensionClasses
                    saveInExtensionClass(extensionClasses, clazz, n);
                }
            }
        }
    }

loadClass方法實現了擴充套件點的分類快取功能,如包裝類,自適應擴充套件點實現類,普通擴充套件點實現類等分別進行快取。需要注意的是自適應擴充套件點實現類@Adaptive註解,該註解原始碼如下

     *For example, given <code>String[] {"key1", "key2"}</code>:
     * <ol>
     * <li>find parameter 'key1' in URL, use its value as the extension's name</li>
     * <li>try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL</li>
     * <li>use default extension if 'key2' doesn't exist either</li>
     * <li>otherwise, throw {@link IllegalStateException}</li>
     * @return
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface Adaptive {
        String[] value() default {};
    }

該註解的作用是決定哪個自適應擴充類被注入,該目標擴充類是由URL中的引數決定,URL中引數key由該註解的value給出,該key的value作為目標擴充類名稱。

  • 如果註解中有多個值,則根據下標從小到大去URL中查詢有無對應的key,一旦找到就用該key的value作為目標擴充類名稱。
  • 如果這些值在url中都沒有對應的key,使用spi上的預設值。

@Adaptive註解可以作用的類上與方法上, 絕大部分情況下,該註解是作用在方法上,當 Adaptive 註解在類上時,Dubbo 不會為該類生成代理類。註解在方法(介面方法)上時, Dubbo 則會為該方法生成代理類Adaptive 註解在介面方法上,表示擴充的載入邏輯需由框架自動生成。註解在類上,表示擴充的載入邏輯由人工編碼完成。

上述的 loadClass掃描的是作用在類上。在 Dubbo 中,僅有兩個類被@Adaptive註解了,分別是  AdaptiveCompiler 和  AdaptiveExtensionFactory
loadClass方法設定快取 cacheAdaptiveClass會導致介面的 cacheAdaptiveClass不為空,後面都會預設用這個擴充類,優先順序最高。

回到主線,當執行完 loadClass方法,配置檔案中的所有擴充類已經被載入到map中,到此,關於快取類載入的過程就分析完了。

Dubbo IOC

getExtensionClasses()方法執行流程完成後,再根據擴充項name從map中取出相應的擴充類即可獲取擴充套件類Class,透過反射建立例項,並透過 injectExtension(instance);方法向例項中注入依賴  這一部分在下一篇文章中將會介紹

DUBBO AOP

當執行完 injectExtension(T instance)方法,在 createExtension(String name)就開始執行 wrapper的包裝,類似於spring中的AOP,dubbo運用了裝飾器模式。

        Set<Class<?>> wrapperClasses = cachedWrapperClasses;        if (CollectionUtils.isNotEmpty(wrapperClasses)) {            // 迴圈建立 Wrapper 例項,形成Wrapper包裝鏈
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }

這裡的 cachedWrapperClasses透過前面的分析已經知道,就是在解析配置檔案時判斷是否是 Wrapper型別的擴充類,++判斷依據為構造方法中是否有引數為該介面類++,則快取到cachedWrapperClasses。

執行 wrapperClass.getConstructor(type).newInstance(instance)將獲取包裝類的構造方法,方法的引數就是該介面型別,並透過反射生成一個包含該擴充類例項的包裝物件,再透過 injectExtension注入包裝物件的依賴。如此迴圈,得到成Wrapper包裝鏈。這裡需注意的是, 配置檔案中內容靠後的包裝類會包裝在相對外層。下面是DUBBO AOP的例子,我們繼續使用上面的Car介面與實現類,同時新增一個實現類,程式碼如下

public class CarWrapper implements Car{    private Car car;    /**
     * 有一個包含該介面類引數的構造方法
     */
    public CarWrapper(Car car) {        this.car = car;
    }
    @Override    public String getBrand() {
        System.out.println("校驗");
        String result = car.getBrand();
        System.out.println("記錄日誌");        return result;
    }
}

該介面實現了Car,並且持有一個Car物件,同時擁有一個構造方法且該構造方法的引數為Car介面型別,那麼該類會被識別為介面的Wrapper類。則可以在方法中做一些切面功能的擴充套件,再呼叫car物件執行其方法實現AOP功能。

將配置檔案內容中新增wrapper實現類,如下

benz=com.xiaoju.automarket.energy.scm.rpc.Benz
bm=com.xiaoju.automarket.energy.scm.rpc.BM
com.xiaoju.automarket.energy.scm.rpc.CarWrapper #包裝類

執行如下程式碼獲取benz的擴充類例項後,呼叫其方法,將會被Wrapper包裝類

public class TestAOP {    public static void main(String[] args) {
        ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
        Car car = carExtensionLoader.getExtension("benz");
        System.out.println(car.getBrand(null));
    }
}

結果如下

校驗
benz car
記錄日誌
Benz

與我們預想的一致,實現了Wrapper類的切面功能。

總結

本篇簡單分別介紹了 Java SPI 與 Dubbo SPI 用法,並對 Dubbo SPI 的載入擴充類的過程進行了分析。同時分析了Dubbo AOP的實現原理。如果文章中有錯誤不妥之處,希望大家指正。

下一篇文章將講述  Dubbo SPI 的擴充套件點自適應機制,dubbo自動注入中也涉及到了該部分,即SpringExtensionFactory執行factory.getExtension涉及到載入自適應擴充點。

作者:丁鵬


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2678246/,如需轉載,請註明出處,否則將追究法律責任。

相關文章