先點贊再看,養(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ū)留言
參考
- Introduction to the Service Provider Interfaces - Oracle
- Dubbo SPI - Apache Dubbo
- Creating Your Own Auto-configuration - Spring
原創(chuàng)不易,未經(jīng)授權(quán)禁止轉(zhuǎn)載套利。如果我的文章對您有幫助推励,請點贊/收藏/關(guān)注鼓勵支持一下吧??????