<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI 的本質是將介面實現類的全限定名設定在檔案中,並由服務載入器讀取組態檔,載入實現類。這樣可以在執行時,動態為介面替換實現類。正因此特性,我們可以很容易的通過 SPI 機制為我們的程式提供拓展功能。
本文主要是特性 & 用法介紹,不涉及原始碼解析(原始碼都很簡單,相信你一定一看就懂)
舉個栗子,現在我們設計了一款全新的紀錄檔框架:「super-logger」。預設以XML檔案作為我們這款紀錄檔的組態檔,並設計了一個組態檔解析的介面:
package com.github.kongwu.spisamples; public interface SuperLoggerConfiguration { void configure(String configFile); }
然後來一個預設的XML實現:
package com.github.kongwu.spisamples; public class XMLConfiguration implements SuperLoggerConfiguration{ public void configure(String configFile){ ...... } }
那麼我們在初始化,解析設定時,只需要呼叫這個XMLConfiguration來解析XML組態檔即可
package com.github.kongwu.spisamples; public class LoggerFactory { static { SuperLoggerConfiguration configuration = new XMLConfiguration(); configuration.configure(configFile); } public static getLogger(Class clazz){ ...... } }
這樣就完成了一個基礎的模型,看起來也沒什麼問題。不過擴充套件性不太好,因為如果想客製化/擴充套件/重寫解析功能的話,我還得重新定義入口的程式碼,LoggerFactory 也得重寫,不夠靈活,侵入性太強了。
比如現在使用者/使用方想增加一個 yml 檔案的方式,作為紀錄檔組態檔,那麼只需要新建一個YAMLConfiguration,實現 SuperLoggerConfiguration 就可以。但是……怎麼注入呢,怎麼讓 LoggerFactory中使用新建的這個 YAMLConfiguration ?難不成連 LoggerFactory 也重寫了?
如果藉助SPI機制的話,這個事情就很簡單了,可以很方便的完成這個入口的擴充套件功能。
下面就先來看看,利用JDK 的 SPI 機制怎麼解決上面的擴充套件性問題。
JDK 中 提供了一個 SPI 的功能,核心類是 java.util.ServiceLoader。其作用就是,可以通過類名獲取在"META-INF/services/"下的多個設定實現檔案。
為了解決上面的擴充套件問題,現在我們在META-INF/services/
下建立一個com.github.kongwu.spisamples.SuperLoggerConfiguration
檔案(沒有字尾)。檔案中只有一行程式碼,那就是我們預設的com.github.kongwu.spisamples.XMLConfiguration
(注意,一個檔案裡也可以寫多個實現,回車分隔)
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration: com.github.kongwu.spisamples.XMLConfiguration
然後通過 ServiceLoader 獲取我們的 SPI 機制設定的實現類:
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator(); SuperLoggerConfiguration configuration; while(iterator.hasNext()) { //載入並初始化實現類 configuration = iterator.next(); } //對最後一個configuration類呼叫configure方法 configuration.configure(configFile);
最後在調整LoggerFactory中初始化設定的方式為現在的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();//載入並初始化實現類 } configuration.configure(configFile); } public static getLogger(Class clazz){ ...... } }
「等等,這裡為什麼是用 iterator ? 而不是get之類的只獲取一個範例的方法?」
試想一下,如果是一個固定的get方法,那麼get到的是一個固定的範例,SPI 還有什麼意義呢?
SPI 的目的,就是增強擴充套件性。將固定的設定提取出來,通過 SPI 機制來設定。那既然如此,一般都會有一個預設的設定,然後通過 SPI 的檔案設定不同的實現,這樣就會存在一個介面多個實現的問題。要是找到多個實現的話,用哪個實現作為最後的範例呢?
所以這裡使用iterator來獲取所有的實現類設定。剛才已經在我們這個 「super-logger」 包裡增加了預設的SuperLoggerConfiguration 實現。
為了支援 YAML 設定,現在在使用方/使用者的程式碼裡,增加一個YAMLConfiguration的 SPI 設定:
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration: com.github.kongwu.spisamples.ext.YAMLConfiguration
此時通過iterator方法,就會獲取到預設的XMLConfiguration和我們擴充套件的這個YAMLConfiguration兩個設定實現類了。
在上面那段載入的程式碼裡,我們遍歷iterator,遍歷到最後,我們**使用最後一個實現設定作為最終的範例。
「再等等?最後一個?怎麼算最後一個?」
使用方/使用者自定義的的這個 YAMLConfiguration 一定是最後一個嗎?
這個真的不一定,取決於我們執行時的 ClassPath 設定,在前面載入的jar自然在前,最後的jar裡的自然當然也在後面。所以「如果使用者的包在ClassPath中的順序比super-logger的包更靠後,才會處於最後一個位置;如果使用者的包位置在前,那麼所謂的最後一個仍然是預設的XMLConfiguration。」
舉個栗子,如果我們程式的啟動指令碼為:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main
預設的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 獲取的最後一個元素又變成了預設的XMLConfiguration,我們使用 JDK SPI 沒啥意義了,獲取的又是第一個,還是預設的XMLConfiguration。
由於這個載入順序(classpath)是由使用者指定的,所以無論我們載入第一個還是最後一個,都有可能會導致載入不到使用者自定義的那個設定。
「所以這也是JDK SPI機制的一個劣勢,無法確認具體載入哪一個實現,也無法載入某個指定的實現,僅靠ClassPath的順序是一個非常不嚴謹的方式」
Dubbo 就是通過 SPI 機制載入所有的元件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模組。基於 SPI,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學習 Dubbo 的原始碼,SPI 機制務必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然後再來分析 Dubbo SPI 的原始碼。
Dubbo 中實現了一套新的 SPI 機制,功能更強大,也更復雜一些。相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以載入指定的實現類。Dubbo SPI 所需的組態檔需放置在 META-INF/dubbo 路徑下,設定內容如下(以下demo來自dubbo官方檔案)。
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現類設定不同,Dubbo SPI 是通過鍵值對的方式進行設定,這樣我們可以按需載入指定的實現類。另外在使用時還需要在介面上標註 @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 最大的區別就在於支援“別名”」,可以通過某個擴充套件點的別名來獲取固定的擴充套件點。就像上面的例子中,我可以獲取 Robot 多個 SPI 實現中別名為“optimusPrime”的實現,也可以獲取別名為“bumblebee”的實現,這個功能非常有用!
通過 @SPI 註解的 value 屬性,還可以預設一個“別名”的實現。比如在Dubbo 中,預設的是Dubbo 私有協定:「dubbo protocol - dubbo://」**
來看看Dubbo中協定的介面:
@SPI("dubbo") public interface Protocol { ...... }
在 Protocol 介面上,增加了一個 @SPI 註解,而註解的 value 值為 Dubbo ,通過 SPI 獲取實現時就會獲取 Protocol SPI 設定中別名為dubbo的那個實現,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對應的那個擴充套件實現了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); //protocol: DubboProtocol
還有一個 Adaptive 的機制,雖然非常靈活,但……用法並不是很“優雅”,這裡就不介紹了
Dubbo 的 SPI 中還有一個“載入優先順序”,優先載入內建(internal)的,然後載入外部的(external),按優先順序順序載入,「如果遇到重複就跳過不會載入」了。
所以如果想靠classpath載入順序去覆蓋內建的擴充套件,也是個不太理智的做法,原因同上 - 載入順序不嚴謹
Spring 的 SPI 組態檔是一個固定的檔案 - META-INF/spring.factories
,功能上和 JDK 的類似,每個介面可以有多個擴充套件實現,使用起來非常簡單:
//獲取所有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 會優先載入專案中的檔案,而不是依賴包中的檔案。所以如果在你的專案中定義個spring.factories檔案,那麼你專案中的檔案會被第一個載入,得到的Factories中,專案中spring.factories裡設定的那個實現類也會排在第一個
如果我們要擴充套件某個介面的話,只需要在你的專案(spring boot)裡新建一個META-INF/spring.factories
檔案,「只新增你要的那個設定,不要完整的複製一遍 Spring Boot 的 spring.factories 檔案然後修改」**
比如我只想新增一個新的 LoggingSystemFactory 實現,那麼我只需要新建一個META-INF/spring.factories
檔案,而不是完整的複製+修改:
org.springframework.boot.logging.LoggingSystemFactory= com.example.log4j2demo.Log4J2LoggingSystem.Factory
檔案方式 | 每個擴充套件點單獨一個檔案 | 每個擴充套件點單獨一個檔案 | 所有的擴充套件點在一個檔案 |
獲取某個固定的實現 | 不支援,只能按順序獲取所有實現 | 有“別名”的概念,可以通過名稱獲取擴充套件點的某個固定實現,配合Dubbo SPI的註解很方便 | 不支援,只能按順序獲取所有實現。但由於Spring Boot ClassLoader會優先載入使用者程式碼中的檔案,所以可以保證使用者自定義的spring.factoires檔案在第一個,通過獲取第一個factory的方式就可以固定獲取自定義的擴充套件 |
其他 | 無 | 支援Dubbo內部的依賴注入,通過目錄來區分Dubbo 內建SPI和外部SPI,優先載入內部,保證內部的優先順序最高 | 無 |
檔案完整度 | 文章 & 三方資料足夠豐富 | 檔案 & 三方資料足夠豐富 | 檔案不夠豐富,但由於功能少,使用非常簡單 |
IDE支援 | 無 | 無 | IDEA 完美支援,有語法提示 |
三種 SPI 機制對比之下,JDK 內建的機制是最弱雞的,但是由於是 JDK 內建,所以還是有一定應用場景,畢竟不用額外的依賴;Dubbo 的功能最豐富,但機制有點複雜了,而且只能配合 Dubbo 使用,不能完全算是一個獨立的模組;Spring 的功能和JDK的相差無幾,最大的區別是所有擴充套件點寫在一個 spring.factories 檔案中,也算是一個改進,並且 IDEA 完美支援語法提示。
到此這篇關於Java Spring Dubbo三種SPI機制的區別的文章就介紹到這了,更多相關Java SPI機制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45