搞懂dubbo的SPI擴(kuò)展機(jī)制

引言

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

先上例子:

  1. 定義接口Animal :
public interface Animal {
   void run();
}
  1. 編寫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("小狗飛奔~");
   }
}
  1. 接下來在 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
  1. 編寫方法進(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)如下:

目錄結(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機(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):

  1. 需要遍歷所有的實(shí)現(xiàn)并實(shí)例化,無法只加載某個(gè)指定的實(shí)現(xiàn)類狂秦,加載機(jī)制不夠靈活;

  2. 配置文件中沒有給實(shí)現(xiàn)類命名侧啼,無法在程序中準(zhǔn)確的引用它們牛柒;

  3. 沒有使用緩存痊乾,每次調(diào)用load方法都需要重新加載

如果想使用Dubbo SPI,接口必須打上@SPI注解哪审。相比之下,Dubbo SPI有以下幾點(diǎn)改進(jìn):

  1. 配置文件改為鍵值對(duì)形式湿滓,可以獲取任一實(shí)現(xiàn)類,而無需加載所有實(shí)現(xiàn)類叽奥,節(jié)約資源;

  2. 增加了緩存來存儲(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注解才能生效侦鹏。


歷史文章:基于事件驅(qū)動(dòng)架構(gòu)的用戶成長(zhǎng)體系

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末臀叙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匹耕,更是在濱河造成了極大的恐慌,老刑警劉巖稳其,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異煤傍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蚯姆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門洒敏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來龄恋,“玉大人凶伙,你說我怎么就攤上這事『伲” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵乘碑,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我兽肤,道長(zhǎng),這世上最難降的妖魔是什么轿衔? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任睦疫,我火速辦了婚禮,結(jié)果婚禮上蛤育,老公的妹妹穿的比我還像新娘葫松。我一直安慰自己,他們只是感情好腋么,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布亥揖。 她就那樣靜靜地躺著,像睡著了一般费变。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挚歧,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音滑负,去河邊找鬼。 笑死矮慕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辟狈。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼明未!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起趟妥,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亲雪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疚膊,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年灌砖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了璧函。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片基显。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖撩幽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情窜醉,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布雨膨,位于F島的核電站,受9級(jí)特大地震影響聊记,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜排监,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一杰捂、第九天 我趴在偏房一處隱蔽的房頂上張望舆床。 院中可真熱鬧嫁佳,春花似錦、人聲如沸蒿往。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔬充,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間饥漫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工愕提, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留馒稍,地道東北人浅侨。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像如输,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子不见,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348