javascript
JDK/Dubbo/Spring 三种 SPI 机制,谁更好呢?
JDK/Dubbo/Spring 三種 SPI 機(jī)制,誰(shuí)更好?
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)類。正因此特性,我們可以很容易的通過(guò) SPI 機(jī)制為我們的程序提供拓展功能。
本文主要是特性 & 用法介紹,不涉及源碼解析(源碼都很簡(jiǎn)單,相信你一定一看就懂)
SPI 有什么用?
舉個(gè)栗子,現(xiàn)在我們?cè)O(shè)計(jì)了一款全新的日志框架:super-logger。默認(rèn)以XML文件作為我們這款日志的配置文件,并設(shè)計(jì)了一個(gè)配置文件解析的接口:
package com.github.kongwu.spisamples;public interface SuperLoggerConfiguration {void configure(String configFile); }然后來(lái)一個(gè)默認(rèn)的XML實(shí)現(xiàn):
package com.github.kongwu.spisamples;public class XMLConfiguration implements SuperLoggerConfiguration{public void configure(String configFile){......} }那么我們?cè)诔跏蓟?#xff0c;解析配置時(shí),只需要調(diào)用這個(gè)XMLConfiguration來(lái)解析XML配置文件即可。
package com.github.kongwu.spisamples;public class LoggerFactory {static {SuperLoggerConfiguration configuration = new XMLConfiguration();configuration.configure(configFile);}public static getLogger(Class clazz){......} }這樣就完成了一個(gè)基礎(chǔ)的模型,看起來(lái)也沒(méi)什么問(wèn)題。不過(guò)擴(kuò)展性不太好,因?yàn)槿绻攵ㄖ?擴(kuò)展/重寫解析功能的話,我還得重新定義入口的代碼,LoggerFactory 也得重寫,不夠靈活,侵入性太強(qiáng)了。
比如現(xiàn)在用戶/使用方想增加一個(gè) yml 文件的方式,作為日志配置文件,那么只需要新建一個(gè)YAMLConfiguration,實(shí)現(xiàn) SuperLoggerConfiguration 就可以。但是……怎么注入呢,怎么讓 LoggerFactory中使用新建的這個(gè) YAMLConfiguration ?難不成連 LoggerFactory 也重寫了?
如果借助SPI機(jī)制的話,這個(gè)事情就很簡(jiǎn)單了,可以很方便的完成這個(gè)入口的擴(kuò)展功能。
下面就先來(lái)看看,利用JDK 的 SPI 機(jī)制怎么解決上面的擴(kuò)展性問(wèn)題。
JDK SPI
JDK 中 提供了一個(gè) SPI 的功能,核心類是 java.util.ServiceLoader。其作用就是,可以通過(guò)類名獲取在"META-INF/services/"下的多個(gè)配置實(shí)現(xiàn)文件。
為了解決上面的擴(kuò)展問(wèn)題,現(xiàn)在我們?cè)贛ETA-INF/services/下創(chuàng)建一個(gè)com.github.kongwu.spisamples.SuperLoggerConfiguration文件(沒(méi)有后綴)。文件中只有一行代碼,那就是我們默認(rèn)的com.github.kongwu.spisamples.XMLConfiguration(注意,一個(gè)文件里也可以寫多個(gè)實(shí)現(xiàn),回車分隔)
然后通過(guò) ServiceLoader 獲取我們的 SPI 機(jī)制配置的實(shí)現(xiàn)類:
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator(); SuperLoggerConfiguration configuration;while(iterator.hasNext()) {//加載并初始化實(shí)現(xiàn)類configuration = iterator.next(); }//對(duì)最后一個(gè)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();//加載并初始化實(shí)現(xiàn)類}configuration.configure(configFile);}public static getLogger(Class clazz){......} }等等,這里為什么是用 iterator ? 而不是get之類的只獲取一個(gè)實(shí)例的方法?
試想一下,如果是一個(gè)固定的get方法,那么get到的是一個(gè)固定的實(shí)例,SPI 還有什么意義呢?
SPI 的目的,就是增強(qiáng)擴(kuò)展性。將固定的配置提取出來(lái),通過(guò) SPI 機(jī)制來(lái)配置。那既然如此,一般都會(huì)有一個(gè)默認(rèn)的配置,然后通過(guò) SPI 的文件配置不同的實(shí)現(xiàn),這樣就會(huì)存在一個(gè)接口多個(gè)實(shí)現(xiàn)的問(wèn)題。要是找到多個(gè)實(shí)現(xiàn)的話,用哪個(gè)實(shí)現(xiàn)作為最后的實(shí)例呢?
所以這里使用iterator來(lái)獲取所有的實(shí)現(xiàn)類配置。剛才已經(jīng)在我們這個(gè) super-logger 包里增加了默認(rèn)的SuperLoggerConfiguration 實(shí)現(xiàn)。
為了支持 YAML 配置,現(xiàn)在在使用方/用戶的代碼里,增加一個(gè)YAMLConfiguration的 SPI 配置:
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:com.github.kongwu.spisamples.ext.YAMLConfiguration此時(shí)通過(guò)iterator方法,就會(huì)獲取到默認(rèn)的XMLConfiguration和我們擴(kuò)展的這個(gè)YAMLConfiguration兩個(gè)配置實(shí)現(xiàn)類了。
在上面那段加載的代碼里,我們遍歷iterator,遍歷到最后,我們**使用最后一個(gè)實(shí)現(xiàn)配置作為最終的實(shí)例。
再等等?最后一個(gè)?怎么算最后一個(gè)?
使用方/用戶自定義的的這個(gè) YAMLConfiguration 一定是最后一個(gè)嗎?
這個(gè)真的不一定,取決于我們運(yùn)行時(shí)的 ClassPath 配置,在前面加載的jar自然在前,最后的jar里的自然當(dāng)然也在后面。所以如果用戶的包在ClassPath中的順序比super-logger的包更靠后,才會(huì)處于最后一個(gè)位置;如果用戶的包位置在前,那么所謂的最后一個(gè)仍然是默認(rèn)的XMLConfiguration。
舉個(gè)栗子,如果我們程序的啟動(dòng)腳本為:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main默認(rèn)的XMLConfiguration SPI配置在super-logger.jar,擴(kuò)展的YAMLConfiguration SPI配置文件在main.jar,那么iterator獲取的最后一個(gè)元素一定為YAMLConfiguration。
但這個(gè)classpath順序如果反了呢?main.jar 在前,super-logger.jar 在后
java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main這樣一來(lái),iterator 獲取的最后一個(gè)元素又變成了默認(rèn)的XMLConfiguration,我們使用 JDK SPI 沒(méi)啥意義了,獲取的又是第一個(gè),還是默認(rèn)的XMLConfiguration。
由于這個(gè)加載順序(classpath)是由用戶指定的,所以無(wú)論我們加載第一個(gè)還是最后一個(gè),都有可能會(huì)導(dǎo)致加載不到用戶自定義的那個(gè)配置。
所以這也是JDK SPI機(jī)制的一個(gè)劣勢(shì),無(wú)法確認(rèn)具體加載哪一個(gè)實(shí)現(xiàn),也無(wú)法加載某個(gè)指定的實(shí)現(xiàn),僅靠ClassPath的順序是一個(gè)非常不嚴(yán)謹(jǐn)?shù)姆绞?/p>
Dubbo SPI
Dubbo 就是通過(guò) SPI 機(jī)制加載所有的組件。不過(guò),Dubbo 并未使用 Java 原生的 SPI 機(jī)制,而是對(duì)其進(jìn)行了增強(qiáng),使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個(gè)非常重要的模塊。基于 SPI,我們可以很容易的對(duì) Dubbo 進(jìn)行拓展。如果大家想要學(xué)習(xí) Dubbo 的源碼,SPI 機(jī)制務(wù)必弄懂。接下來(lái),我們先來(lái)了解一下 Java SPI 與 Dubbo SPI 的用法,然后再來(lái)分析 Dubbo SPI 的源碼。
Dubbo 中實(shí)現(xiàn)了一套新的 SPI 機(jī)制,功能更強(qiáng)大,也更復(fù)雜一些。相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過(guò) ExtensionLoader,我們可以加載指定的實(shí)現(xiàn)類。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內(nèi)容如下(以下demo來(lái)自dubbo官方文檔)。
與 Java SPI 實(shí)現(xiàn)類配置不同,Dubbo SPI 是通過(guò)鍵值對(duì)的方式進(jìn)行配置,這樣我們可以按需加載指定的實(shí)現(xiàn)類。另外在使用時(shí)還需要在接口上標(biāo)注 @SPI 注解。下面來(lái)演示 Dubbo SPI 的用法:
@SPI public interface Robot {void sayHello(); }public class OptimusPrime implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Optimus Prime.");} }public class Bumblebee implements Robot {@Overridepublic void sayHello() {System.out.println("Hello, I am Bumblebee.");} }public class DubboSPITest {@Testpublic 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ū)別就在于支持“別名”,可以通過(guò)某個(gè)擴(kuò)展點(diǎn)的別名來(lái)獲取固定的擴(kuò)展點(diǎn)。就像上面的例子中,我可以獲取 Robot 多個(gè) SPI 實(shí)現(xiàn)中別名為“optimusPrime”的實(shí)現(xiàn),也可以獲取別名為“bumblebee”的實(shí)現(xiàn),這個(gè)功能非常有用!
通過(guò) @SPI 注解的 value 屬性,還可以默認(rèn)一個(gè)“別名”的實(shí)現(xiàn)。比如在Dubbo 中,默認(rèn)的是Dubbo 私有協(xié)議:dubbo protocol - dubbo://
**
來(lái)看看Dubbo中協(xié)議的接口:
在 Protocol 接口上,增加了一個(gè) @SPI 注解,而注解的 value 值為 Dubbo ,通過(guò) SPI 獲取實(shí)現(xiàn)時(shí)就會(huì)獲取 Protocol SPI 配置中別名為dubbo的那個(gè)實(shí)現(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.MockProtocoldubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocolinjvm=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然后只需要通過(guò)getDefaultExtension,就可以獲取到 @SPI 注解上value對(duì)應(yīng)的那個(gè)擴(kuò)展實(shí)現(xiàn)了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); //protocol: DubboProtocol還有一個(gè) Adaptive 的機(jī)制,雖然非常靈活,但……用法并不是很“優(yōu)雅”,這里就不介紹了
Dubbo 的 SPI 中還有一個(gè)“加載優(yōu)先級(jí)”,優(yōu)先加載內(nèi)置(internal)的,然后加載外部的(external),按優(yōu)先級(jí)順序加載,如果遇到重復(fù)就跳過(guò)不會(huì)加載了。
所以如果想靠classpath加載順序去覆蓋內(nèi)置的擴(kuò)展,也是個(gè)不太理智的做法,原因同上 - 加載順序不嚴(yán)謹(jǐn)
Spring SPI
Spring 的 SPI 配置文件是一個(gè)固定的文件 - META-INF/spring.factories,功能上和 JDK 的類似,每個(gè)接口可以有多個(gè)擴(kuò)展實(shí)現(xiàn),使用起來(lái)非常簡(jiǎ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 中,將所有的配置放到一個(gè)固定的文件中,省去了配置一大堆文件的麻煩。至于多個(gè)接口的擴(kuò)展配置,是用一個(gè)文件好,還是每個(gè)單獨(dú)一個(gè)文件好這個(gè),這個(gè)問(wèn)題就見仁見智了(個(gè)人喜歡 Spring 這種,干凈利落)。
Spring的SPI 雖然屬于spring-framework(core),但是目前主要用在spring boot中……
和前面兩種 SPI 機(jī)制一樣,Spring 也是支持 ClassPath 中存在多個(gè) spring.factories 文件的,加載時(shí)會(huì)按照 classpath 的順序依次加載這些 spring.factories 文件,添加到一個(gè) ArrayList 中。由于沒(méi)有別名,所以也沒(méi)有去重的概念,有多少就添加多少。
但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 會(huì)優(yōu)先加載項(xiàng)目中的文件,而不是依賴包中的文件。所以如果在你的項(xiàng)目中定義個(gè)spring.factories文件,那么你項(xiàng)目中的文件會(huì)被第一個(gè)加載,得到的Factories中,項(xiàng)目中spring.factories里配置的那個(gè)實(shí)現(xiàn)類也會(huì)排在第一個(gè)
如果我們要擴(kuò)展某個(gè)接口的話,只需要在你的項(xiàng)目(spring boot)里新建一個(gè)META-INF/spring.factories文件,只添加你要的那個(gè)配置,不要完整的復(fù)制一遍 Spring Boot 的 spring.factories 文件然后修改
比如我只想添加一個(gè)新的 LoggingSystemFactory 實(shí)現(xiàn),那么我只需要新建一個(gè)META-INF/spring.factories文件,而不是完整的復(fù)制+修改:
org.springframework.boot.logging.LoggingSystemFactory=\ com.example.log4j2demo.Log4J2LoggingSystem.Factory對(duì)比
三種 SPI 機(jī)制對(duì)比之下,JDK 內(nèi)置的機(jī)制是最弱雞的,但是由于是 JDK 內(nèi)置,所以還是有一定應(yīng)用場(chǎng)景,畢竟不用額外的依賴;Dubbo 的功能最豐富,但機(jī)制有點(diǎn)復(fù)雜了,而且只能配合 Dubbo 使用,不能完全算是一個(gè)獨(dú)立的模塊;Spring 的功能和JDK的相差無(wú)幾,最大的區(qū)別是所有擴(kuò)展點(diǎn)寫在一個(gè) spring.factories 文件中,也算是一個(gè)改進(jìn),并且 IDEA 完美支持語(yǔ)法提示。
各位看官們大佬們,你們覺(jué)得 JDK/Dubbo/Spring 三種 SPI 的機(jī)制,哪個(gè)更好呢?歡迎評(píng)論區(qū)留言
文章來(lái)源:https://www.jianshu.com/p/487896e02f66
作者:空無(wú)c
總結(jié)
以上是生活随笔為你收集整理的JDK/Dubbo/Spring 三种 SPI 机制,谁更好呢?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: iOS线程锁中你还不知道的内容
- 下一篇: Java并发知识总结,超详细!(上)