引言
SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制爲我們的程序提供拓展功能。
在談dubbo的SPI擴展機制之前,我們需要先了解下java原生的SPI機制,有助於我們更好的瞭解dubbo的SPI。
java原生的SPI
先上例子:
1. 定義接口Animal :
public interface Animal {
void run();
}
2. 編寫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("小狗飛奔~");
}
}
3. 接下來在 META-INF/services 文件夾下創建一個文件,名稱爲 Animal 的全限定名 com.sunnick.animal.Animal,文件內容爲實現類的全限定的類名,如下:
com.sunnick.animal.impl.Dog
com.sunnick.animal.impl.Cat
4. 編寫方法進行測試
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實際上是“接口+策略模式+配置文件”實現的動態加載機制。在系統設計中,模塊之間通常基於接口編程,不直接顯示指定實現類。一旦代碼裏指定了實現類,就無法在不修改代碼的情況下替換爲另一種實現。爲了達到動態可插拔的效果,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)
throw new IllegalArgumentException("Extension name == null");
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);
}
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;
}
創建實例過程如下,即createExtension()方法:
private T createExtension(String name) {
// 從配置文件中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關係表
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);
}
//此處省略一些源碼......
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
獲取所有的SPI配置文件,並解析配置文件中的鍵值對的方法getExtensionClasses()的源碼如下:
private Map<String, Class<?>> getExtensionClasses() {
// 從緩存中獲取已加載的拓展類
Map<String, Class<?>> classes = cachedClasses.get();
// 雙重檢查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加載拓展類
classes = loadExtensionClasses();
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有以下幾個缺點:
- 需要遍歷所有的實現並實例化,無法只加載某個指定的實現類,加載機制不夠靈活;
- 配置文件中沒有給實現類命名,無法在程序中準確的引用它們;
- 沒有使用緩存,每次調用load方法都需要重新加載
如果想使用Dubbo SPI,接口必須打上@SPI註解。相比之下,Dubbo SPI有以下幾點改進:
- 配置文件改爲鍵值對形式,可以獲取任一實現類,而無需加載所有實現類,節約資源;
- 增加了緩存來存儲實例,提高了讀取的性能;
除此之外,dubbo SPI還提供了默認值的指定方式(例如可通過@SPI(“cat”)方式指定Animal的默認實現類爲Cat)。同時dubbo SPI還提供了對IOC和AOP等高級功能的支持,以實現更多類型的擴展。
總結
SPI是一種服務發現機制,提供了動態發現實現類的能力,體現了分層解耦的思想。
在架構設計和代碼編寫過程中,模塊之間應該針對接口編程,避免直接引用具體的實現類,可達到可插拔的效果。
Dubbo提供了增強版的SPI機制,在使用過程中,需要在接口上打上@SPI註解才能生效。