JDK/Dubbo/Spring 三種 SPI 機制葫辐,誰更好搜锰?

先點贊再看,養(yǎng)成好習(xí)慣

SPI 全稱為 Service Provider Interface耿战,是一種服務(wù)發(fā)現(xiàn)機制蛋叼。SPI 的本質(zhì)是將接口實現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件剂陡,加載實現(xiàn)類狈涮。這樣可以在運行時,動態(tài)為接口替換實現(xiàn)類鸭栖。正因此特性歌馍,我們可以很容易的通過 SPI 機制為我們的程序提供拓展功能。

本文主要是特性 & 用法介紹晕鹊,不涉及源碼解析(源碼都很簡單松却,相信你一定一看就懂)

SPI 有什么用?

舉個栗子溅话,現(xiàn)在我們設(shè)計了一款全新的日志框架:super-logger晓锻。默認(rèn)以XML文件作為我們這款日志的配置文件,并設(shè)計了一個配置文件解析的接口:

package com.github.kongwu.spisamples;

public interface SuperLoggerConfiguration {
    void configure(String configFile);
}

然后來一個默認(rèn)的XML實現(xiàn):

package com.github.kongwu.spisamples;

public class XMLConfiguration implements SuperLoggerConfiguration{
    public void configure(String configFile){
        ......
    }
}

那么我們在初始化飞几,解析配置時砚哆,只需要調(diào)用這個XMLConfiguration來解析XML配置文件即可。

package com.github.kongwu.spisamples;

public class LoggerFactory {
    static {
        SuperLoggerConfiguration configuration = new XMLConfiguration();
        configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){
        ......
    }
}

這樣就完成了一個基礎(chǔ)的模型屑墨,看起來也沒什么問題窟社。不過擴展性不太好,因為如果想定制/擴展/重寫解析功能的話绪钥,我還得重新定義入口的代碼灿里,LoggerFactory 也得重寫,不夠靈活程腹,侵入性太強了匣吊。

比如現(xiàn)在用戶/使用方想增加一個 yml 文件的方式,作為日志配置文件寸潦,那么只需要新建一個YAMLConfiguration色鸳,實現(xiàn) SuperLoggerConfiguration 就可以。但是……怎么注入呢见转,怎么讓 LoggerFactory中使用新建的這個 YAMLConfiguration 命雀?難不成連 LoggerFactory 也重寫了?

如果借助SPI機制的話斩箫,這個事情就很簡單了吏砂,可以很方便的完成這個入口的擴展功能撵儿。

下面就先來看看,利用JDK 的 SPI 機制怎么解決上面的擴展性問題狐血。

JDK SPI

JDK 中 提供了一個 SPI 的功能淀歇,核心類是 java.util.ServiceLoader。其作用就是匈织,可以通過類名獲取在"META-INF/services/"下的多個配置實現(xiàn)文件浪默。

為了解決上面的擴展問題,現(xiàn)在我們在META-INF/services/下創(chuàng)建一個com.github.kongwu.spisamples.SuperLoggerConfiguration文件(沒有后綴)缀匕。文件中只有一行代碼纳决,那就是我們默認(rèn)的com.github.kongwu.spisamples.XMLConfiguration(注意,一個文件里也可以寫多個實現(xiàn)乡小,回車分隔)

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.XMLConfiguration

然后通過 ServiceLoader 獲取我們的 SPI 機制配置的實現(xiàn)類:

ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;

while(iterator.hasNext()) {
    //加載并初始化實現(xiàn)類
    configuration = iterator.next();
}

//對最后一個configuration類調(diào)用configure方法
configuration.configure(configFile);

最后在調(diào)整LoggerFactory中初始化配置的方式為現(xiàn)在的SPI方式:

package com.github.kongwu.spisamples;

public class LoggerFactory {
    static {
        ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
        Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
        SuperLoggerConfiguration configuration;

        while(iterator.hasNext()) {
            configuration = iterator.next();//加載并初始化實現(xiàn)類
        }
        configuration.configure(configFile);
    }
    
    public static getLogger(Class clazz){
        ......
    }
}

等等岳链,這里為什么是用 iterator ? 而不是get之類的只獲取一個實例的方法?

試想一下劲件,如果是一個固定的get方法,那么get到的是一個固定的實例约急,SPI 還有什么意義呢零远?

SPI 的目的,就是增強擴展性厌蔽。將固定的配置提取出來牵辣,通過 SPI 機制來配置。那既然如此奴饮,一般都會有一個默認(rèn)的配置纬向,然后通過 SPI 的文件配置不同的實現(xiàn),這樣就會存在一個接口多個實現(xiàn)的問題戴卜。要是找到多個實現(xiàn)的話逾条,用哪個實現(xiàn)作為最后的實例呢?

所以這里使用iterator來獲取所有的實現(xiàn)類配置投剥。剛才已經(jīng)在我們這個 super-logger 包里增加了默認(rèn)的SuperLoggerConfiguration 實現(xiàn)师脂。

為了支持 YAML 配置,現(xiàn)在在使用方/用戶的代碼里江锨,增加一個YAMLConfiguration的 SPI 配置:

META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:

com.github.kongwu.spisamples.ext.YAMLConfiguration

此時通過iterator方法吃警,就會獲取到默認(rèn)的XMLConfiguration和我們擴展的這個YAMLConfiguration兩個配置實現(xiàn)類了。

在上面那段加載的代碼里啄育,我們遍歷iterator酌心,遍歷到最后,我們**使用最后一個實現(xiàn)配置作為最終的實例挑豌。

再等等安券?最后一個墩崩?怎么算最后一個?

使用方/用戶自定義的的這個 YAMLConfiguration 一定是最后一個嗎完疫?

這個真的不一定泰鸡,取決于我們運行時的 ClassPath 配置,在前面加載的jar自然在前壳鹤,最后的jar里的自然當(dāng)然也在后面盛龄。所以如果用戶的包在ClassPath中的順序比super-logger的包更靠后,才會處于最后一個位置芳誓;如果用戶的包位置在前余舶,那么所謂的最后一個仍然是默認(rèn)的XMLConfiguration。

舉個栗子锹淌,如果我們程序的啟動腳本為:

java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main

默認(rèn)的XMLConfiguration SPI配置在super-logger.jar匿值,擴展的YAMLConfiguration SPI配置文件在main.jar,那么iterator獲取的最后一個元素一定為YAMLConfiguration赂摆。

但這個classpath順序如果反了呢挟憔?main.jar 在前,super-logger.jar 在后

java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main

這樣一來烟号,iterator 獲取的最后一個元素又變成了默認(rèn)的XMLConfiguration绊谭,我們使用 JDK SPI 沒啥意義了,獲取的又是第一個汪拥,還是默認(rèn)的XMLConfiguration达传。

由于這個加載順序(classpath)是由用戶指定的,所以無論我們加載第一個還是最后一個迫筑,都有可能會導(dǎo)致加載不到用戶自定義的那個配置宪赶。

所以這也是JDK SPI機制的一個劣勢,無法確認(rèn)具體加載哪一個實現(xiàn)脯燃,也無法加載某個指定的實現(xiàn)搂妻,僅靠ClassPath的順序是一個非常不嚴(yán)謹(jǐn)?shù)姆绞?/strong>

Dubbo SPI

Dubbo 就是通過 SPI 機制加載所有的組件。不過辕棚,Dubbo 并未使用 Java 原生的 SPI 機制叽讳,而是對其進(jìn)行了增強,使其能夠更好的滿足需求坟募。在 Dubbo 中岛蚤,SPI 是一個非常重要的模塊⌒概矗基于 SPI涤妒,我們可以很容易的對 Dubbo 進(jìn)行拓展。如果大家想要學(xué)習(xí) Dubbo 的源碼赚哗,SPI 機制務(wù)必弄懂她紫。接下來硅堆,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然后再來分析 Dubbo SPI 的源碼贿讹。

Dubbo 中實現(xiàn)了一套新的 SPI 機制渐逃,功能更強大,也更復(fù)雜一些民褂。相關(guān)邏輯被封裝在了 ExtensionLoader 類中茄菊,通過 ExtensionLoader,我們可以加載指定的實現(xiàn)類赊堪。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下面殖,配置內(nèi)容如下(以下demo來自dubbo官方文檔)。

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

與 Java SPI 實現(xiàn)類配置不同哭廉,Dubbo SPI 是通過鍵值對的方式進(jìn)行配置脊僚,這樣我們可以按需加載指定的實現(xiàn)類。另外在使用時還需要在接口上標(biāo)注 @SPI 注解遵绰。下面來演示 Dubbo SPI 的用法:

@SPI
public interface Robot {
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}


public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

Dubbo SPI 和 JDK SPI 最大的區(qū)別就在于支持“別名”辽幌,可以通過某個擴展點的別名來獲取固定的擴展點。就像上面的例子中椿访,我可以獲取 Robot 多個 SPI 實現(xiàn)中別名為“optimusPrime”的實現(xiàn)乌企,也可以獲取別名為“bumblebee”的實現(xiàn),這個功能非常有用赎离!

通過 @SPI 注解的 value 屬性,還可以默認(rèn)一個“別名”的實現(xiàn)端辱。比如在Dubbo 中梁剔,默認(rèn)的是Dubbo 私有協(xié)議:dubbo protocol - dubbo://
**
來看看Dubbo中協(xié)議的接口:

@SPI("dubbo")
public interface Protocol {
    ......
}

在 Protocol 接口上,增加了一個 @SPI 注解舞蔽,而注解的 value 值為 Dubbo 荣病,通過 SPI 獲取實現(xiàn)時就會獲取 Protocol SPI 配置中別名為dubbo的那個實現(xiàn),com.alibaba.dubbo.rpc.Protocol文件如下:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol


dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol


injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper

然后只需要通過getDefaultExtension渗柿,就可以獲取到 @SPI 注解上value對應(yīng)的那個擴展實現(xiàn)了

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol

還有一個 Adaptive 的機制个盆,雖然非常靈活蒙幻,但……用法并不是很“優(yōu)雅”拌牲,這里就不介紹了

Dubbo 的 SPI 中還有一個“加載優(yōu)先級”,優(yōu)先加載內(nèi)置(internal)的拂铡,然后加載外部的(external)陨溅,按優(yōu)先級順序加載终惑,如果遇到重復(fù)就跳過不會加載了。

所以如果想靠classpath加載順序去覆蓋內(nèi)置的擴展门扇,也是個不太理智的做法雹有,原因同上 - 加載順序不嚴(yán)謹(jǐn)

Spring SPI

Spring 的 SPI 配置文件是一個固定的文件 - META-INF/spring.factories偿渡,功能上和 JDK 的類似,每個接口可以有多個擴展實現(xiàn)霸奕,使用起來非常簡單:

//獲取所有factories文件中配置的LoggingSystemFactory
List<LoggingSystemFactory>> factories = 
    SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

下面是一段 Spring Boot 中 spring.factories 的配置

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

......

Spring SPI 中溜宽,將所有的配置放到一個固定的文件中,省去了配置一大堆文件的麻煩质帅。至于多個接口的擴展配置适揉,是用一個文件好,還是每個單獨一個文件好這個临梗,這個問題就見仁見智了(個人喜歡 Spring 這種涡扼,干凈利落)。

Spring的SPI 雖然屬于spring-framework(core)盟庞,但是目前主要用在spring boot中……

和前面兩種 SPI 機制一樣吃沪,Spring 也是支持 ClassPath 中存在多個 spring.factories 文件的,加載時會按照 classpath 的順序依次加載這些 spring.factories 文件什猖,添加到一個 ArrayList 中票彪。由于沒有別名,所以也沒有去重的概念不狮,有多少就添加多少降铸。

但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 會優(yōu)先加載項目中的文件摇零,而不是依賴包中的文件推掸。所以如果在你的項目中定義個spring.factories文件,那么你項目中的文件會被第一個加載驻仅,得到的Factories中谅畅,項目中spring.factories里配置的那個實現(xiàn)類也會排在第一個

如果我們要擴展某個接口的話,只需要在你的項目(spring boot)里新建一個META-INF/spring.factories文件噪服,只添加你要的那個配置毡泻,不要完整的復(fù)制一遍 Spring Boot 的 spring.factories 文件然后修改
**
比如我只想添加一個新的 LoggingSystemFactory 實現(xiàn),那么我只需要新建一個META-INF/spring.factories文件粘优,而不是完整的復(fù)制+修改:

org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory

對比

JDK SPI DUBBO SPI Spring SPI
文件方式 每個擴展點單獨一個文件 每個擴展點單獨一個文件 所有的擴展點在一個文件
獲取某個固定的實現(xiàn) 不支持仇味,只能按順序獲取所有實現(xiàn) 有“別名”的概念,可以通過名稱獲取擴展點的某個固定實現(xiàn)雹顺,配合Dubbo SPI的注解很方便 不支持丹墨,只能按順序獲取所有實現(xiàn)。但由于Spring Boot ClassLoader會優(yōu)先加載用戶代碼中的文件嬉愧,所以可以保證用戶自定義的spring.factoires文件在第一個带到,通過獲取第一個factory的方式就可以固定獲取自定義的擴展
其他 支持Dubbo內(nèi)部的依賴注入,通過目錄來區(qū)分Dubbo 內(nèi)置SPI和外部SPI,優(yōu)先加載內(nèi)部揽惹,保證內(nèi)部的優(yōu)先級最高
文檔完整度 文章 & 三方資料足夠豐富 文檔 & 三方資料足夠豐富 文檔不夠豐富被饿,但由于功能少,使用非常簡單
IDE支持 IDEA 完美支持搪搏,有語法提示

三種 SPI 機制對比之下狭握,JDK 內(nèi)置的機制是最弱雞的,但是由于是 JDK 內(nèi)置疯溺,所以還是有一定應(yīng)用場景论颅,畢竟不用額外的依賴;Dubbo 的功能最豐富囱嫩,但機制有點復(fù)雜了恃疯,而且只能配合 Dubbo 使用,不能完全算是一個獨立的模塊墨闲;Spring 的功能和JDK的相差無幾今妄,最大的區(qū)別是所有擴展點寫在一個 spring.factories 文件中,也算是一個改進(jìn)鸳碧,并且 IDEA 完美支持語法提示盾鳞。

各位看官們大佬們,你們覺得 JDK/Dubbo/Spring 三種 SPI 的機制瞻离,哪個更好呢腾仅?歡迎評論區(qū)留言

參考

原創(chuàng)不易,未經(jīng)授權(quán)禁止轉(zhuǎn)載套利。如果我的文章對您有幫助推励,請點贊/收藏/關(guān)注鼓勵支持一下吧??????

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肉迫,隨后出現(xiàn)的幾起案子验辞,更是在濱河造成了極大的恐慌,老刑警劉巖昂拂,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件受神,死亡現(xiàn)場離奇詭異抛猖,居然都是意外死亡格侯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門财著,熙熙樓的掌柜王于貴愁眉苦臉地迎上來联四,“玉大人,你說我怎么就攤上這事撑教〕眨” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵伟姐,是天一觀的道長收苏。 經(jīng)常有香客問我亿卤,道長,這世上最難降的妖魔是什么鹿霸? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任排吴,我火速辦了婚禮,結(jié)果婚禮上懦鼠,老公的妹妹穿的比我還像新娘钻哩。我一直安慰自己,他們只是感情好肛冶,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布街氢。 她就那樣靜靜地躺著,像睡著了一般睦袖。 火紅的嫁衣襯著肌膚如雪珊肃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天扣泊,我揣著相機與錄音近范,去河邊找鬼。 笑死延蟹,一個胖子當(dāng)著我的面吹牛评矩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播阱飘,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼斥杜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沥匈?” 一聲冷哼從身側(cè)響起蔗喂,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎高帖,沒想到半個月后缰儿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡散址,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年乖阵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片预麸。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞪浸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吏祸,到底是詐尸還是另有隱情对蒲,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站蹈矮,受9級特大地震影響砰逻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜泛鸟,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一诱渤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谈况,春花似錦勺美、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至祝闻,卻和暖如春占卧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背联喘。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工华蜒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人豁遭。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓叭喜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蓖谢。 傳聞我的和親對象是個殘疾皇子捂蕴,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容