先點贊再看,養成好習慣
SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣能夠在運行時,動態爲接口替換實現類。正所以特性,咱們能夠很容易的經過 SPI 機制爲咱們的程序提供拓展功能。
本文主要是特性 & 用法介紹,不涉及源碼解析(源碼都很簡單,相信你必定一看就懂)html
舉個栗子,如今咱們設計了一款全新的日誌框架:super-logger。默認以XML文件做爲咱們這款日誌的配置文件,並設計了一個配置文件解析的接口:java
package com.github.kongwu.spisamples; public interface SuperLoggerConfiguration { void configure(String configFile); }
而後來一個默認的XML實現:git
package com.github.kongwu.spisamples; public class XMLConfiguration implements SuperLoggerConfiguration{ public void configure(String configFile){ ...... } }
那麼咱們在初始化,解析配置時,只須要調用這個XMLConfiguration來解析XML配置文件便可。github
package com.github.kongwu.spisamples; public class LoggerFactory { static { SuperLoggerConfiguration configuration = new XMLConfiguration(); configuration.configure(configFile); } public static getLogger(Class clazz){ ...... } }
這樣就完成了一個基礎的模型,看起來也沒什麼問題。不過擴展性不太好,由於若是想定製/擴展/重寫解析功能的話,我還得從新定義入口的代碼,LoggerFactory 也得重寫,不夠靈活,侵入性太強了。web
好比如今用戶/使用方想增長一個 yml 文件的方式,做爲日誌配置文件,那麼只須要新建一個YAMLConfiguration,實現 SuperLoggerConfiguration 就能夠。可是……怎麼注入呢,怎麼讓 LoggerFactory中使用新建的這個 YAMLConfiguration ?難不成連 LoggerFactory 也重寫了?redis
若是藉助SPI機制的話,這個事情就很簡單了,能夠很方便的完成這個入口的擴展功能。spring
下面就先來看看,利用JDK 的 SPI 機制怎麼解決上面的擴展性問題。apache
JDK 中 提供了一個 SPI 的功能,核心類是 java.util.ServiceLoader。其做用就是,能夠經過類名獲取在"META-INF/services/"下的多個配置實現文件。bash
爲了解決上面的擴展問題,如今咱們在META-INF/services/
下建立一個com.github.kongwu.spisamples.SuperLoggerConfiguration
文件(沒有後綴)。文件中只有一行代碼,那就是咱們默認的com.github.kongwu.spisamples.XMLConfiguration
(注意,一個文件裏也能夠寫多個實現,回車分隔)oracle
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
JDK SPI | DUBBO SPI | Spring SPI | |
---|---|---|---|
文件方式 | 每一個擴展點單獨一個文件 | 每一個擴展點單獨一個文件 | 全部的擴展點在一個文件 |
獲取某個固定的實現 | 不支持,只能按順序獲取全部實現 | 有「別名」的概念,能夠經過名稱獲取擴展點的某個固定實現,配合Dubbo SPI的註解很方便 | 不支持,只能按順序獲取全部實現。但因爲Spring Boot ClassLoader會優先加載用戶代碼中的文件,因此能夠保證用戶自定義的spring.factoires文件在第一個,經過獲取第一個factory的方式就能夠固定獲取自定義的擴展 |
其餘 | 無 | 支持Dubbo內部的依賴注入,經過目錄來區分Dubbo 內置SPI和外部SPI,優先加載內部,保證內部的優先級最高 | 無 |
文檔完整度 | 文章 & 三方資料足夠豐富 | 文檔 & 三方資料足夠豐富 | 文檔不夠豐富,但因爲功能少,使用很是簡單 |
IDE支持 | 無 | 無 | IDEA 完美支持,有語法提示 |
三種 SPI 機制對比之下,JDK 內置的機制是最弱雞的,可是因爲是 JDK 內置,因此仍是有必定應用場景,畢竟不用額外的依賴;Dubbo 的功能最豐富,但機制有點複雜了,並且只能配合 Dubbo 使用,不能徹底算是一個獨立的模塊;Spring 的功能和JDK的相差無幾,最大的區別是全部擴展點寫在一個 spring.factories 文件中,也算是一個改進,而且 IDEA 完美支持語法提示。
各位看官們大佬們,大家以爲 JDK/Dubbo/Spring 三種 SPI 的機制,哪一個更好呢?歡迎評論區留言
原創不易,未經受權禁止轉載。若是個人文章對您有幫助,請點贊/收藏/關注鼓勵支持一下吧❤❤❤❤❤❤