1. 程式人生 > >Dubbo之SPI原理詳解

Dubbo之SPI原理詳解

       SPI全稱為Service Provider Interface,是一種服務提供機制,比如在現實中我們經常會有這種場景,就是對於一個規範定義方而言(可以理解為一個或多個介面),具體的服務實現方是不可知的(可以理解為對這些介面的實現類),那麼在定義這些規範的時候,就需要規範定義方能夠通過一定的方式來獲取到這些服務提供方具體提供的是哪些服務,而SPI就是進行這種定義的。

1. jdk與Dubbo SPI對比

       比較典型的SPI定義方式有jdk的SPI,其主要是在專案的META-INF/services目錄下以介面的全路徑名為檔名,然後在該檔案中宣告該介面的具體實現類有哪些。這裡說的META-INF/services

目錄和其中的檔案主要是在服務提供方的jar包中。這裡的整體流程可以理解為,首先規範制定方會定義一個介面,比如com.jdk.spi.Fruit,然後其通過讀取專案目錄下(包括所依賴的jar包中)的META-INF/service名稱為com.jdk.spi.Fruit的檔案,獲取其中定義的子類,接著讀取並例項化該類的物件,作為目標介面的實現類來使用。通過這種方式,服務提供方只需要在其提供的jar包的META-INF/services目錄中宣告其提供的子類服務是什麼即可。對於jdk的SPI,其主要存在兩個問題:

  • 為每個介面提供的服務一般儘量只提供一個,因為jdk的SPI預設會將所有目標檔案中定義的所有子類都讀取到返回使用;
  • 當定義多個子類實現時,無法動態的根據配置來使用不同的配置;

       基於上述兩個問題,dubbo對jdk的SPI進行了擴充套件。從整體流程上而言,dubbo的SPI與jdk的SPI相似,也就是在規範制定方會定義一定的介面,然後讀取專案的META-INF/dubbo/internal目錄下的以介面全路徑名定義的檔案,最後通過所需要的key值來獲取不同的子類實現。

2. Dubbo SPI示例

       這裡假設對於規範提供方定義了一個com.dubbo.spi.Fruit的介面,該介面的具體宣告如下:

@SPI
public interface Fruit {
  void eat();
}

       這裡的@SPI註解的主要用於標識當前介面是一個dubbo的SPI介面。如下是規範制定方使用該介面的方式:

public class ExtensionFactoryApp {
  public static void main(String[] args) {
    Fruit apple = ExtensionLoader.getExtensionLoader(Fruit.class).getExtension("banana");
    apple.eat();
  }
}

       這裡ExtensionLoader.getExtensionLoader()方法會讀取META-INF/dubbo/internal目錄下的名稱為com.dubbo.spi.Fruit的檔案,然後getExtension("banana")方法會獲取該檔案中key值為banana的子類,並且進行例項化返回。通過這種方式,我們的規範定義方就不需要知道具體的Fruit實現類是什麼,而只需要按照一定的規則進行讀取並使用即可。

       對於服務提供方而言,其首先需要在META-INF/dubbo/internal目錄下定義一個名稱為com.dubbo.spi.Fruit的檔案,如下是該檔案的內容:

apple=com.dubbo.spi.impl.Apple
banana=com.dubbo.spi.impl.Banana

       可以看到,這裡為Fruit介面定義了兩個實現類AppleBanana,對應的key分別為apple和banana。我們執行上述程式,可以看到如下輸出:

eat banana

3. 實現原理

       通過上面的示例可以看出,dubbo對於SPI的實現主要是在ExtensionLoader這個類中,這個類主要有三個方法:

public T getExtension(String name);
public T getAdaptiveExtension();
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type);
  • getExtension():主要用於獲取名稱為name的對應的子類的物件,這裡如果子類物件如果有AOP相關的配置,這裡也會對其進行封裝;
  • getAdaptiveExtension():使用定義的裝飾類來封裝目標子類,具體使用哪個子類可以在定義的裝飾類中通過一定的條件進行配置;
  • getExtensionLoader():載入當前介面的子類並且例項化一個ExtensionLoader物件。

3.1 getExtension()

       這裡getExtension()方法的主要作用是獲取name對應的子類物件返回。其實現方式是首先讀取定義檔案中的子類,然後根據不同的子類物件的功能的不同,比如使用@Adaptive修飾的裝飾類和用於AOP的Wrapper類,將其封裝到不同的快取中。最後根據傳入的name獲取其對應的子類物件,並且使用相應的Wrapper類對其進行封裝。如下是getExtension()方法的原始碼:

@SuppressWarnings("unchecked")
public T getExtension(String name) {
    if (name == null || name.length() == 0) {
        throw new IllegalArgumentException("Extension name == null");
    }
    
    // 如果名稱為true,則返回預設的子類物件,這裡預設的子類物件的name定義在目標介面的@SPI註解中
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    
    // 檢視當前是否已經快取有儲存目標物件的例項的Holder物件,快取了則直接返回,
    // 沒快取則建立一個並快取起來
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    
    // 如果無法從Holder中獲取目標物件的例項,則使用雙檢查法為目標物件建立一個例項
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 建立name對應的子類物件的例項
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

       可以看到,關於對於目標物件的獲取,首先是從快取裡取,沒取到才會進行建立。這裡需要說明的是,如果傳入的name為true,那麼就會返回預設的子類例項,而預設的子類例項是通過其名稱進行對映的,該名稱儲存在目標介面的@SPI註解中。如下是createExtension()方法的原始碼:

@SuppressWarnings("unchecked")
private T createExtension(String name) {
    // 獲取當前名稱對應的子類型別,如果不存在,則丟擲異常
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    
    try {
        // 獲取當前class對應的例項,如果快取中不存在,則例項化一個並快取起來
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        
        // 為生成的例項通過其set方法注入對應的例項,這裡例項的獲取方式不僅可以通過SPI的方式
        // 也可以通過Spring的bean工廠獲取
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                // 例項化各個wrapper物件,並將目標物件通過wrapper的構造方法傳入,
                // 另外還會通過wrapper物件的set方法對其依賴的屬性進行注入
                instance = injectExtension((T) wrapperClass.getConstructor(type)
                               .newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " 
            + type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}

       在createExtension()方法中,其主要做了三件事:a. 載入定義檔案中的各個子類,然後將目標name對應的子類返回;b. 通過目標子類的set方法為其注入其所依賴的bean,這裡既可以通過SPI,也可以通過Spring的BeanFactory獲取所依賴的bean;c. 獲取定義檔案中定義的wrapper物件,然後使用該wrapper物件封裝目標物件,並且還會呼叫其set方法為wrapper物件注入其所依賴的屬性。

       關於wrapper物件,這裡需要說明的是,其主要作用是為目標物件實現AOP。wrapper物件有兩個特點:a. 與目標物件實現了同一個介面;b. 有一個以目標介面為引數型別的建構函式。這也就是上述createExtension()方法最後封裝wrapper物件時傳入的建構函式例項始終可以為instance例項的原因。這裡我們首先看getExtensionClasses()方法是如何實現的:

private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 載入定義檔案,並且將定義的類按照功能快取在不同的屬性中,即:
                // a. 目標class型別快取在cachedClasses;
                // b. wrapper的class型別快取在cachedWrapperClasses;
                // c. 用於裝飾的class型別快取在cachedAdaptiveClass;
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

       上述程式碼中,主要是在快取中獲取目標class的map快取,如果不存在則通過定義檔案載入。這裡我們繼續看loadExtensionClasses()的原始碼:

private Map<String, Class<?>> loadExtensionClasses() {
    // 獲取目標介面上通過@SPI註解定義的預設子類對應的名稱,並將其快取在cachedDefaultName中
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension " 
                    + "name on extension " + type.getName()
                    + ": " + Arrays.toString(names));
            }
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

    // 分別在META-INF/dubbo/internal、META-INF/dubbo、META-INF/services目錄下
    // 獲取定義檔案,並且讀取定義檔案中的內容,這裡主要是通過META-INF/dubbo/internal
    // 獲取目標定義檔案
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, 
        type.getName().replace("org.apache", "com.alibaba"));
    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()主要是分別從三個目錄中讀取定義檔案,讀取該檔案,並且進行快取。這裡我們繼續看loadDirectory()方法的原始碼:

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir,
        String type) {
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        
        // 載入定義檔案
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            // 對定義檔案進行遍歷,依次載入定義檔案的內容
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                     type + ", description file: " + fileName + ").", t);
    }
}

       這裡主要是對每個目錄進行載入,然後依次載入定義檔案的內容,而對定義檔案內容的處理主要是在loadResource()方法中,在對檔案中每一行記錄進行處理之後,其其最終是呼叫的loadClass()方法載入目標class的。如下是loadClass()方法的原始碼:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, 
        Class<?> clazz, String name) throws NoSuchMethodException {
    // 如果載入得到的子類不是目標介面的實現類,則丟擲異常
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error when load extension class(interface: " 
            + type + ", class line: " + clazz.getName() + "), class "
            + clazz.getName() + "is not subtype of interface.");
    }
    
    // 如果子類上標註有@Adaptive註解,說明其是一個裝飾類,則將其快取在cachedAdaptiveClass中,
    // 需要注意的是,一個介面只能為其定義一個裝飾類
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("More than 1 adaptive class found: "
                + cachedAdaptiveClass.getClass().getName()
                + ", " + clazz.getClass().getName());
        }
    // 這裡判斷子類是否是一個wrapper類,判斷方式就是檢查其是否有隻含一個目標介面型別引數的建構函式,
    // 有則說明其是一個AOP的wrapper類
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        wrappers.add(clazz);
    } else {
        // 走到這裡說明當前子類不是一個功能型的類,而是最終實現具體目標的子類
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " 
                    + clazz.getName() + " in the config " + resourceURL);
            }
        }
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            // 獲取子類上的@Activate註解,該註解的主要作用是對子類進行分組的,
            // 對於分組之後的子類,可以通過getActivateExtension()來獲取
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                cachedActivates.put(names[0], activate);
            } else {
                // 相容alibaba版本的註解
                com.alibaba.dubbo.common.extension.Activate oldActivate = 
                   clazz.getAnnotation(com.alibaba.dubbo.common.extension.Activate.class);
                if (oldActivate != null) {
                    cachedActivates.put(names[0], oldActivate);
                }
            }
            
            // 將目標子類快取到extensionClasses中
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("Duplicate extension " 
                        + type.getName() + " name " + n + " on " + c.getName() 
                        + " and " + clazz.getName());
                }
            }
        }
    }
}

       這裡loadClass()方法主要作用是對子類進行劃分,這裡主要劃分成了三部分:a. 使用@Adaptive註解標註的裝飾類;b. 包含有目標介面型別引數建構函式的wrapper類;c. 目標處理具體業務的子類。

       總結而言,getExtension()方法主要是獲取指定名稱對應的子類。在獲取過程中,首先會從快取中獲取是否已經載入過該子類,如果沒載入過,則通過定義檔案載入,並且使用獲取到的wrapper物件封裝目標物件返回。

3.2 getAdaptiveExtension()

       前面我們講解了ExtensionLoader在載入了定義檔案之後會對子類進行一個劃分,其中就涉及到使用@Adaptive進行標註的子類,該子類的作用主要是用於對目標類進行裝飾的,從而實現一定的目的。但是@Adaptive也可以標註在方法上,其使用的方式主要是在目標介面的某個方法上進行標註,這個時候,dubbo就會通過javassist位元組碼生成工具來動態的生成目標介面的子類物件,該子類會對該介面中標註了@Adaptive註解的方法進行重寫,而其餘的方法則預設丟擲異常,通過這種方式可以達到對特定的方法進行修飾的目的。我們首先來閱讀getAdaptiveExtension()方法的原始碼:

@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
    // 從快取中獲取裝飾類的例項,存在則直接返回,不存在則建立一個快取起來,然後返回
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        if (createAdaptiveInstanceError == null) {
            synchronized (cachedAdaptiveInstance) {
                instance = cachedAdaptiveInstance.get();
                if (instance == null) {
                    try {
                        // 建立一個裝飾類的例項
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("fail to create adaptive" 
                            + " instance: " + t.toString(), t);
                    }
                }
            }
        } else {
            throw new IllegalStateException("fail to create adaptive instance: " 
                + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
        }
    }

    return (T) instance;
}

       這裡主要是從快取中獲取目標類的例項,不存在則建立一個該例項,這裡我們直接閱讀createAdaptiveExtension()方法的原始碼:

@SuppressWarnings("unchecked")
private T createAdaptiveExtension() {
    try {
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can not create adaptive extension "
            + type + ", cause: " + e.getMessage(), e);
    }
}

       這裡createAdaptiveExtension()首先委託給getAdaptiveExtensionClass()方法獲取一個裝飾類例項,然後通過injectExtension()方法呼叫該例項的set方法來注入其所依賴的屬性值。如下是getAdaptiveExtensionClass()方法的原始碼:

private Class<?> getAdaptiveExtensionClass() {
    // 獲取目標extensionClasses,如果無法獲取到,則在定義檔案中進行載入
    getExtensionClasses();
    // 如果目標型別有使用@Adaptive標註的子型別,則直接使用該子類作為裝飾類
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    
    // 如果目標型別沒有使用@Adaptive標註的子型別,則嘗試在目標介面中查詢是否有使用@Adaptive標註的
    // 方法,如果有,則為該方法動態生成子類裝飾程式碼
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

       這裡可以看到,只有目標介面沒有使用@Adaptive標註的子類時,才會使用Javassist來為目標介面生成其子類的裝飾方法。如下是createAdaptiveExtensionClass()方法的原始碼:

private Class<?> createAdaptiveExtensionClass() {
    // 建立子類程式碼的字串物件
    String code = createAdaptiveExtensionClassCode();
    ClassLoader classLoader = findClassLoader();
    // 獲取當前dubbo SPI中定義的Compiler介面的子類物件,預設是使用javassist,
    // 然後通過該物件來編譯生成的code,從而動態生成一個class物件
    org.apache.dubbo.common.compiler.Compiler compiler = 
     ExtensionLoader.getExtensionLoader(
        org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

       上述程式碼中,首先動態生成目標介面的子類字串,然後通過javassit來編譯該子類字串,從而動態生成目標class。關於如何編譯目的碼字串,這裡主要有兩種方式:jdk編譯和javassist編譯,讀者可自行查閱其具體的編譯原理,我們這裡主要講解dubbo SPI是如何生成子類程式碼的。由於createAdaptiveExtensionClassCode()方法主要是通過一些字串的拼接操作來得到子類字串,我們這裡就直接展示dubbo中一個非常重要的介面Protocol,其生成的子類字串的形式:

package org.apache.dubbo.rpc;

import org.apache.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
  private static final org.apache.dubbo.common.logger.Logger logger = 
      org.apache.dubbo.common.logger.LoggerFactory.getLogger(ExtensionLoader.class);
  private java.util.concurrent.atomic.AtomicInteger count = 
      new java.util.concurrent.atomic.AtomicInteger(0);

  // 對於未使用@Adaptive標註的方法,子類中直接丟擲異常
  public void destroy() {
    throw new UnsupportedOperationException("method public abstract void" 
        + " org.apache.dubbo.rpc.Protocol.destroy() of interface " 
        + "org.apache.dubbo.rpc.Protocol is not adaptive method!");
  }

  // 對於未使用@Adaptive標註的方法,子類中直接丟擲異常
  public int getDefaultPort() {
    throw new UnsupportedOperationException("method public abstract int " 
        + "org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface " 
        + "org.apache.dubbo.rpc.Protocol is not adaptive method!");
  }

  // 對於使用@Adaptive標註的類,則在子類中委託給其他的類進行處理
  public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) 
      throws org.apache.dubbo.rpc.RpcException {
    if (arg0 == null) {
      throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
    }

    if (arg0.getUrl() == null) {
      throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument " 
          + "getUrl() == null");
    }

    org.apache.dubbo.common.URL url = arg0.getUrl();
    String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());

    if (extName == null) {
      throw new IllegalStateException("Fail to get " 
          + "extension(org.apache.dubbo.rpc.Protocol) name from url(" 
          + url.toString() + ") use keys([protocol])");
    }

    org.apache.dubbo.rpc.Protocol extension = null;
    try {
      extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
          org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
    } catch (Exception e) {
      if (count.incrementAndGet() == 1) {
        logger.warn("Failed to find extension named " + extName 
            + " for type org.apache.dubbo.rpc.Protocol, will use default extension" 
            + " dubbo instead.", e);
      }
      extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
          org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");
    }

    return extension.export(arg0);
  }

  public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, 
        org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
    if (arg1 == null) {
      throw new IllegalArgumentException("url == null");
    }

    org.apache.dubbo.common.URL url = arg1;
    String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
    if (extName == null) {
      throw new IllegalStateException("Fail to get " 
          + "extension(org.apache.dubbo.rpc.Protocol) name from url(" 
          + url.toString() + ") use keys([protocol])");
    }

    org.apache.dubbo.rpc.Protocol extension = null;
    try {
      extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
          org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
    } catch (Exception e) {
      if (count.incrementAndGet() == 1) {
        logger.warn("Failed to find extension named " + extName + " for type " 
            + "org.apache.dubbo.rpc.Protocol, will use default extension dubbo " 
            + "instead.", e);
      }
      extension = (org.apache.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(
          org.apache.dubbo.rpc.Protocol.class).getExtension("dubbo");
    }

    return extension.refer(arg0, arg1);
  }
}

       可以看到,子類中對於沒有使用@Adaptive標註的方法,其實現時直接丟擲異常,對於使用@Adaptive標註的方法,則對其進行實現,並且委託給其他的SPI提供者進行相關的處理。

3.3 getExtensionLoader()

       這裡getExtensionLoader()的主要作用是為當前介面型別例項化一個ExtensionLoader物件,然後將其快取起來。如下是getExtensionLoader()方法的原始碼:

@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    
    // 判斷當前傳入的如果不是介面,則丟擲異常
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type(" + type 
            + ") is not interface!");
    }
    
    // 這裡主要是判斷傳入的介面上是否使用@SPI進行了標註,標註了該註解才表明當前
    // 介面是一個dubbo的SPI介面
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type(" + type 
            + ") is not extension, because WITHOUT @" 
            + SPI.class.getSimpleName() + " Annotation!");
    }

    // 從快取中讀取當前類對應的ExtensionLoader物件,如果不存在,則例項化一個並且快取起來
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

       這裡可以看到,對於ExtensionLoader的獲取,其實現過程比較簡單,主要是從快取中獲取,如果快取不存在,則例項化一個並且快取起來。

4. 小結

       本文首先對jdk的SPI和dubbo的SPI進行了簡單的對比,說明了dubbo相對於jdk的SPI所提供的額外的動態配置效能;然後通過一個示例來展示了dubbo的SPI的使用方式;最後著重講解了用於實現dubbo SPI的ExtensionLoader的實現原理。