<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JDK/Dubbo/Spring 三種 SPI 機制,誰更好?

          共 9855字,需瀏覽 20分鐘

           ·

          2021-07-20 13:27

          往期熱門文章:

          1、小團隊真的適合引入Spring Cloud微服務(wù)嗎?
          2、IDEA 中的熱部署神器!
          3、我承認 IDEA 2021.3 有點強!
          4、6 月份最火的 10 個 GitHub 項目
          5、推薦 7 個牛哄哄 Spring Cloud 實戰(zhàn)項目

          來源:juejin.cn/post/6950266942875779108

          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。默認以XML文件作為我們這款日志的配置文件,并設(shè)計了一個配置文件解析的接口:

          package com.github.kongwu.spisamples;
          public interface SuperLoggerConfiguration { void configure(String configFile);}

          然后來一個默認的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文件(沒有后綴)。


          文件中只有一行代碼,那就是我們默認的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 機制來配置。那既然如此,一般都會有一個默認的配置,然后通過 SPI 的文件配置不同的實現(xiàn),這樣就會存在一個接口多個實現(xiàn)的問題。要是找到多個實現(xiàn)的話,用哪個實現(xiàn)作為最后的實例呢?


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


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

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

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


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


          再等等?最后一個?怎么算最后一個?


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


          這個真的不一定,取決于我們運行時的 ClassPath 配置,在前面加載的jar自然在前,最后的jar里的自然當(dāng)然也在后面。所以如果用戶的包在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)是由用戶指定的,所以無論我們加載第一個還是最后一個,都有可能會導(dǎo)致加載不到用戶自定義的那個配置。


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


          # Dubbo SPI

          Dubbo 就是通過 SPI 機制加載所有的組件。不過,Dubbo 并未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。在 Dubbo 中,SPI 是一個非常重要的模塊。基于 SPI,我們可以很容易的對 Dubbo 進行拓展。如果大家想要學(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.OptimusPrimebumblebee = org.apache.spi.Bumblebee

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

          @SPIpublic 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 屬性,還可以默認一個“別名”的實現(xiàn)。比如在Dubbo 中,默認的是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.ProtocolFilterWrapperlistener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrappermock=com.alibaba.dubbo.rpc.support.MockProtocol

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

          injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocolrmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocolhessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocolcom.alibaba.dubbo.rpc.protocol.http.HttpProtocolcom.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocolthrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocolmemcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocolredis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocolrest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocolregistry=com.alibaba.dubbo.registry.integration.RegistryProtocolqos=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文件中配置的LoggingSystemFactoryList<LoggingSystemFactory>> factories =     SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);

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

          # Logging Systemsorg.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 Loadersorg.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader
          # ConfigData Location Resolversorg.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

          # 對比



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


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


          參考

          • Introduction to the Service Provider Interfaces - Oracle

          • Dubbo SPI - Apache Dubbo

          • Creating Your Own Auto-configuration - Spring


          版權(quán)申明:內(nèi)容來源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無法確認,都會標(biāo)明作者及出處,如有侵權(quán)煩請告知,我們會立即刪除并致歉。謝謝!


          往期熱門文章:

          1、歷史文章分類導(dǎo)讀列表!精選優(yōu)秀博文都在這里了!》

          2、Java必會的工具庫,讓你的代碼量減少90%
          3、騰訊最大股東收購了 Stack Overflow,以后“抄代碼”都要付費了么?
          4、靈隱寺招聘:沒有KPI,佛系上班……
          5、如何優(yōu)雅處理重復(fù)請求/并發(fā)請求?
          6、不用到2038年,MySQL的TIMESTAMP就能把我們系統(tǒng)搞崩!
          7、翻車!在項目中用了Arrays.asList、ArrayList的subList,被公開批評
          8、想接私活時薪再翻一倍,建議根據(jù)這幾個開源的Spring Boot項目改改~
          9、細數(shù)ThreadLocal三大坑,內(nèi)存泄露僅是小兒科
          10、Redis與MySQL雙寫一致性如何保證?

          瀏覽 49
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色美女毛片 | 中文人妻无码一区二区三 | 影音先锋aV成人无码电影 | 成人69视频 | 欧美操日本|