引言
SPI 全稱為 Service Provider Interface疆股,是一種服務(wù)發(fā)現(xiàn)機(jī)制狸吞。SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類磺平。這樣可以在運(yùn)行時(shí)靴姿,動(dòng)態(tài)為接口替換實(shí)現(xiàn)類沃但。正因此特性,我們可以很容易的通過 SPI 機(jī)制為我們的程序提供拓展功能佛吓。
在談dubbo的SPI擴(kuò)展機(jī)制之前宵晚,我們需要先了解下java原生的SPI機(jī)制,有助于我們更好的了解dubbo的SPI维雇。
java原生的SPI
先上例子:
- 定義接口Animal :
public interface Animal {
void run();
}
- 編寫2個(gè)實(shí)現(xiàn)類淤刃,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("小狗飛奔~");
}
}
- 接下來在 META-INF/services 文件夾下創(chuàng)建一個(gè)文件,名稱為 Animal 的全限定名 com.sunnick.animal.Animal吱型,文件內(nèi)容為實(shí)現(xiàn)類的全限定的類名逸贾,如下:
com.sunnick.animal.impl.Dog
com.sunnick.animal.impl.Cat
- 編寫方法進(jìn)行測(cè)試
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();
}
}
目錄結(jié)構(gòu)如下:
測(cè)試結(jié)果如下:
======this is SPI======
小狗飛奔~
小貓步走起來~
從測(cè)試結(jié)果可以看出,我們的兩個(gè)實(shí)現(xiàn)類被成功的加載津滞,并輸出了相應(yīng)的內(nèi)容铝侵。但我們并沒有在代碼中顯示指定Animal的類型,這就是java原生的SPI機(jī)制在發(fā)揮作用据沈。
SPI機(jī)制如下:
SPI實(shí)際上是“接口+策略模式+配置文件”實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制哟沫。在系統(tǒng)設(shè)計(jì)中,模塊之間通承拷椋基于接口編程嗜诀,不直接顯示指定實(shí)現(xiàn)類。一旦代碼里指定了實(shí)現(xiàn)類孔祸,就無法在不修改代碼的情況下替換為另一種實(shí)現(xiàn)隆敢。為了達(dá)到動(dòng)態(tài)可插拔的效果,java提供了SPI以實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)拂蝎。
在上述例子中,通過ServiceLoader.load(Animal.class)方法動(dòng)態(tài)加載Animal的實(shí)現(xiàn)類温自,通過追蹤該方法的源碼皇钞,發(fā)現(xiàn)程序會(huì)去讀取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()加載類對(duì)象夹界,并用instance()方法將類實(shí)例化馆里,從而完成了服務(wù)發(fā)現(xiàn)。
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機(jī)制丙者,如slf日志門面和log4j、logback等日志實(shí)現(xiàn)营密,jdbc的java,sql.Driver接口和各種數(shù)據(jù)庫的connector的實(shí)現(xiàn)等械媒。
dubbo的SPI使用
Dubbo 并未使用 Java SPI,而是重新實(shí)現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制卵贱。Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader键俱,我們可以加載指定的實(shí)現(xiàn)類世分。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內(nèi)容如下:
dog=com.sunnick.animal.impl.Dog
cat=com.sunnick.animal.impl.Cat
與 Java SPI 實(shí)現(xiàn)類配置不同臭埋,Dubbo SPI 是通過鍵值對(duì)的方式進(jìn)行配置,這樣就可以按需加載指定的實(shí)現(xiàn)類瓢阴。另外,在使用Dubbo SPI 時(shí)荣恐,需要在 Animal接口上標(biāo)注 @SPI 注解,Cat與Dog類不變叠穆。下面來演示 Dubbo SPI 的用法:
@SPI
public interface Animal {
void run();
}
編寫測(cè)試方法:
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();
}
測(cè)試結(jié)果如下:
======dubbo SPI======
小貓步走起來~
小狗飛奔~
dubbo的SPI源碼分析
Dubbo通過ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat")方法獲取實(shí)例。該方法中示损,會(huì)先到緩存列表中獲取實(shí)例嚷硫,若未命中检访,則創(chuàng)建實(shí)例:
public T getExtension(String name) {
if(name != null && name.length() != 0) {
if("true".equals(name)) {
// 獲取默認(rèn)的拓展實(shí)現(xiàn)類
return this.getDefaultExtension();
} else {
// Holder仔掸,顧名思義,用于持有目標(biāo)對(duì)象
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) {
// 創(chuàng)建拓展實(shí)例
instance = this.createExtension(name);
holder.set(instance);
}
}
}
return instance;
}
} else {
throw new IllegalArgumentException("Extension name == null");
}
}
創(chuàng)建實(shí)例過程如下丹禀,即createExtension()方法:
private T createExtension(String name) {
//獲取所有的SPI配置文件,并解析配置文件中的鍵值對(duì)
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) {
//通過反射創(chuàng)建實(shí)例
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配置文件双泪,并解析配置文件中的鍵值對(duì)的方法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 加鎖村斟。加鎖后再次檢查緩存,并判空蟆盹。此時(shí)如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類逾滥。下面分析 loadExtensionClasses 方法的邏輯。
private Map<String, Class<?>> loadExtensionClasses() {
// 獲取 SPI 注解寨昙,這里的 type 變量是在調(diào)用 getExtensionLoader 方法時(shí)傳入的,即示例中的Animal
SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
String extensionClasses = defaultAnnotation.value();
if(extensionClasses != null && (extensionClasses = extensionClasses.trim()).length() > 0) {
// 對(duì) SPI 注解內(nèi)容進(jìn)行切分
String[] names = NAME_SEPARATOR.split(extensionClasses);
// 檢測(cè) SPI 注解內(nèi)容是否合法欢顷,不合法則拋出異常
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;
}
可以看出捉蚤,最后調(diào)用了loadFile方法,該方法就是從指定的目錄下讀取指定的文件名外里,解析其內(nèi)容,將鍵值對(duì)放入map中盅蝗,其過程不在贅述。
以上就是dubbo的SPI加載實(shí)例的過程墩莫。
Dubbo SPI與原生SPI的對(duì)比
java原生SPI有以下幾個(gè)缺點(diǎn):
需要遍歷所有的實(shí)現(xiàn)并實(shí)例化,無法只加載某個(gè)指定的實(shí)現(xiàn)類狂秦,加載機(jī)制不夠靈活;
配置文件中沒有給實(shí)現(xiàn)類命名侧啼,無法在程序中準(zhǔn)確的引用它們牛柒;
沒有使用緩存痊乾,每次調(diào)用load方法都需要重新加載
如果想使用Dubbo SPI,接口必須打上@SPI注解哪审。相比之下,Dubbo SPI有以下幾點(diǎn)改進(jìn):
配置文件改為鍵值對(duì)形式湿滓,可以獲取任一實(shí)現(xiàn)類,而無需加載所有實(shí)現(xiàn)類叽奥,節(jié)約資源;
增加了緩存來存儲(chǔ)實(shí)例铭污,提高了讀取的性能;
除此之外,dubbo SPI還提供了默認(rèn)值的指定方式(例如可通過@SPI(“cat”)方式指定Animal的默認(rèn)實(shí)現(xiàn)類為Cat)岂膳。同時(shí)dubbo SPI還提供了對(duì)IOC和AOP等高級(jí)功能的支持,以實(shí)現(xiàn)更多類型的擴(kuò)展谈截。
總結(jié)
SPI是一種服務(wù)發(fā)現(xiàn)機(jī)制,提供了動(dòng)態(tài)發(fā)現(xiàn)實(shí)現(xiàn)類的能力毙死,體現(xiàn)了分層解耦的思想。
在架構(gòu)設(shè)計(jì)和代碼編寫過程中扼倘,模塊之間應(yīng)該針對(duì)接口編程,避免直接引用具體的實(shí)現(xiàn)類再菊,可達(dá)到可插拔的效果颜曾。
Dubbo提供了增強(qiáng)版的SPI機(jī)制纠拔,在使用過程中泛豪,需要在接口上打上@SPI注解才能生效侦鹏。