搞懂dubbo的SPI擴展機制

引言

SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制爲我們的程序提供拓展功能。

在談dubbo的SPI擴展機制之前,我們需要先了解下java原生的SPI機制,有助於我們更好的瞭解dubbo的SPI。

java原生的SPI

先上例子:

  1. 定義接口Animal :
public interface Animal {
   void run();
}
  1. 編寫2個實現類,Cat和Dog
public class Cat implements Animal{
   @Override
   public void run() {
      System.out.println("小貓步走起來~");
   }
}
public class Dog implements Animal {
   @Override
   public void run() {
      System.out.println("小狗飛奔~");
   }
}
  1. 接下來在 META-INF/services 文件夾下創建一個文件,名稱爲 Animal 的全限定名 com.sunnick.animal.Animal,文件內容爲實現類的全限定的類名,如下:
com.sunnick.animal.impl.Dog  
com.sunnick.animal.impl.Cat
  1. 編寫方法進行測試
public static void main(String[] s){
   System.out.println("======this is SPI======");
   ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);  
       Iterator<Animal> animals = serviceLoader.iterator();  
       while (animals.hasNext()) {  
           animals.next().run();
       }
}

目錄結構如下:

目錄結構
測試結果如下:

======this is SPI======
小狗飛奔~
小貓步走起來~

從測試結果可以看出,我們的兩個實現類被成功的加載,並輸出了相應的內容。但我們並沒有在代碼中顯示指定Animal的類型,這就是java原生的SPI機制在發揮作用。

SPI機制如下:
SPI機制
SPI實際上是“接口+策略模式+配置文件”實現的動態加載機制。在系統設計中,模塊之間通常基於接口編程,不直接顯示指定實現類。一旦代碼裏指定了實現類,就無法在不修改代碼的情況下替換爲另一種實現。爲了達到動態可插拔的效果,java提供了SPI以實現服務發現。

在上述例子中,通過ServiceLoader.load(Animal.class)方法動態加載Animal的實現類,通過追蹤該方法的源碼,發現程序會去讀取META-INF/services目錄下文件名爲類名的配置文件(如上述例子中的META-INF/services/com.sunnick.animal.Animal文件),如下,其中PREFIX 常量值爲”META-INF/services/”:

try {
    String fullName = PREFIX + service.getName();
    if (loader == null)
        configs = ClassLoader.getSystemResources(fullName);
    else
        configs = loader.getResources(fullName);
} catch (IOException x) {
    fail(service, "Error locating configuration files", x);
}

然後再通過反射Class.forName()加載類對象,並用instance()方法將類實例化,從而完成了服務發現。

String cn = nextName;
nextName = null;
Class<?> c = null;
try {
    c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
    fail(service,"Provider " + cn + " not found");
}

許多常用的框架都使用SPI機制,如slf日誌門面和log4j、logback等日誌實現,jdbc的java,sql.Driver接口和各種數據庫的connector的實現等。

dubbo的SPI使用

Dubbo 並未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現類。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內容如下:

dog=com.sunnick.animal.impl.Dog  
cat=com.sunnick.animal.impl.Cat

與 Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣就可以按需加載指定的實現類。另外,在使用Dubbo SPI 時,需要在 Animal接口上標註 @SPI 註解,Cat與Dog類不變。下面來演示 Dubbo SPI 的用法:

@SPI
public interface Animal {
   void run();
}

編寫測試方法:

public void testDubboSPI(){
   System.out.println("======dubbo SPI======");
   ExtensionLoader<Animal> extensionLoader =
         ExtensionLoader.getExtensionLoader(Animal.class);
   Animal cat = extensionLoader.getExtension("cat");
   cat.run();
   Animal dog = extensionLoader.getExtension("dog");
   dog.run();
}

測試結果如下:

======dubbo SPI======
小貓步走起來~
小狗飛奔~

dubbo的SPI源碼分析

Dubbo通過ExtensionLoader.getExtensionLoader(Animal.class).getExtension(“cat”)方法獲取實例。該方法中,會先到緩存列表中獲取實例,若未命中,則創建實例:

public T getExtension(String name) {
    if(name != null && name.length() != 0) {
        if("true".equals(name)) {
    // 獲取默認的拓展實現類
            return this.getDefaultExtension();
        } else {
      // Holder,顧名思義,用於持有目標對象
            Holder holder = (Holder)this.cachedInstances.get(name);
            if(holder == null) {
                this.cachedInstances.putIfAbsent(name, new Holder());
                holder = (Holder)this.cachedInstances.get(name);
            }
            Object instance = holder.get();
      // 雙重檢查
            if(instance == null) {
                synchronized(holder) {
                    instance = holder.get();
                    if(instance == null) {
             // 創建拓展實例
                        instance = this.createExtension(name);
                        holder.set(instance);
                    }
                }
            }
            return instance;
        }
    } else {
        throw new IllegalArgumentException("Extension name == null");
    }
}

創建實例過程如下,即createExtension()方法:

private T createExtension(String name) {
//獲取所有的SPI配置文件,並解析配置文件中的鍵值對
    Class clazz = (Class)this.getExtensionClasses().get(name);
    if(clazz == null) {
        throw this.findException(name);
    } else {
        try {
            Object t = EXTENSION_INSTANCES.get(clazz);
            if(t == null) {
          //通過反射創建實例
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                t = EXTENSION_INSTANCES.get(clazz);
            }
    //此處省略一些源碼 ......
            return t;
        } catch (Throwable var7) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " + this.type + ")  could not be instantiated: " + var7.getMessage(), var7);
        }
    }
}

獲取所有的SPI配置文件,並解析配置文件中的鍵值對的方法getExtensionClasses()的源碼如下:

private Map<String, Class<?>> getExtensionClasses() {
// 從緩存中獲取已加載的拓展類
    Map classes = (Map)this.cachedClasses.get();
//雙重檢查
    if(classes == null) {
        Holder var2 = this.cachedClasses;
        synchronized(this.cachedClasses) {
            classes = (Map)this.cachedClasses.get();
            if(classes == null) {
          //加載拓展類
                classes = this.loadExtensionClasses();
                this.cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

這裏也是先檢查緩存,若緩存未命中,則通過 synchronized 加鎖。加鎖後再次檢查緩存,並判空。此時如果 classes 仍爲 null,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯。

private Map<String, Class<?>> loadExtensionClasses() {
// 獲取 SPI 註解,這裏的 type 變量是在調用 getExtensionLoader 方法時傳入的,即示例中的Animal
    SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String extensionClasses = defaultAnnotation.value();
        if(extensionClasses != null && (extensionClasses = extensionClasses.trim()).length() > 0) {
      // 對 SPI 註解內容進行切分
            String[] names = NAME_SEPARATOR.split(extensionClasses);
      // 檢測 SPI 註解內容是否合法,不合法則拋出異常
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
            }

            if(names.length == 1) {
                this.cachedDefaultName = names[0];
            }
        }
    }
    HashMap extensionClasses1 = new HashMap();
// 加載指定文件夾下的配置文件
    this.loadFile(extensionClasses1, "META-INF/dubbo/internal/");
    this.loadFile(extensionClasses1, "META-INF/dubbo/");
    this.loadFile(extensionClasses1, "META-INF/services/");
    return extensionClasses1;
}

可以看出,最後調用了loadFile方法,該方法就是從指定的目錄下讀取指定的文件名,解析其內容,將鍵值對放入map中,其過程不在贅述。

以上就是dubbo的SPI加載實例的過程。

Dubbo SPI與原生SPI的對比

java原生SPI有以下幾個缺點:

  1. 需要遍歷所有的實現並實例化,無法只加載某個指定的實現類,加載機制不夠靈活;

  2. 配置文件中沒有給實現類命名,無法在程序中準確的引用它們;

  3. 沒有使用緩存,每次調用load方法都需要重新加載

如果想使用Dubbo SPI,接口必須打上@SPI註解。相比之下,Dubbo SPI有以下幾點改進:

  1. 配置文件改爲鍵值對形式,可以獲取任一實現類,而無需加載所有實現類,節約資源;

  2. 增加了緩存來存儲實例,提高了讀取的性能;

除此之外,dubbo SPI還提供了默認值的指定方式(例如可通過@SPI(“cat”)方式指定Animal的默認實現類爲Cat)。同時dubbo SPI還提供了對IOC和AOP等高級功能的支持,以實現更多類型的擴展。

總結

SPI是一種服務發現機制,提供了動態發現實現類的能力,體現了分層解耦的思想。

在架構設計和代碼編寫過程中,模塊之間應該針對接口編程,避免直接引用具體的實現類,可達到可插拔的效果。

Dubbo提供了增強版的SPI機制,在使用過程中,需要在接口上打上@SPI註解才能生效。


歷史文章:基於事件驅動架構的用戶成長體系

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章