溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

java中的spi怎么用

發(fā)布時(shí)間:2022-03-03 13:53:24 來(lái)源:億速云 閱讀:130 作者:小新 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要介紹了java中的spi怎么用,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

    前言

    在開(kāi)發(fā)過(guò)程中,經(jīng)常要用到第三方提供的SDK來(lái)完成一些業(yè)務(wù)擴(kuò)展功能,比如調(diào)用第三方的發(fā)短信、圖片驗(yàn)證碼、人臉識(shí)別等等功能,但問(wèn)題是,第三方SDK只是提供了標(biāo)準(zhǔn)的功能實(shí)現(xiàn),某些場(chǎng)景下,開(kāi)發(fā)者還想基于這些SDK做一些個(gè)性化的定制和擴(kuò)展,那要怎么辦呢?

    于是,一些優(yōu)秀的SDK就通過(guò)SPI的機(jī)制,將一些接口擴(kuò)能開(kāi)放出來(lái),開(kāi)發(fā)者就可以基于這些SPI接口做自身的業(yè)務(wù)擴(kuò)展了;

    總結(jié)一下SPI思想:在系統(tǒng)的各個(gè)模塊中,往往有不同的實(shí)現(xiàn)方案,例如日志模塊的方案、xml解析的方案等,為了在裝載模塊的時(shí)候不具體指明實(shí)現(xiàn)類(lèi),我們需要一種服務(wù)發(fā)現(xiàn)機(jī)制,java spi就提供這樣一種機(jī)制。有點(diǎn)類(lèi)似于IoC的思想,將服務(wù)裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)時(shí)尤其重要

    Java 的SPI機(jī)制在很多框架,中間件等都有著廣泛的使用,如springboot,Dubbo中均有采用,屬于高級(jí)Java開(kāi)發(fā)知識(shí)點(diǎn),有必要掌握

    下面用一張簡(jiǎn)圖說(shuō)明下SPI機(jī)制的原理

    java中的spi怎么用

    一、JDK中SPI的使用規(guī)范

    • 定義通用的服務(wù)接口,針對(duì)服務(wù)接口,提供具體實(shí)現(xiàn)類(lèi)

    • 在jar包的META-INF/services/目錄中,新建一個(gè)文件,文件名為 接口的 “全限定名”, 文件內(nèi)容為該接口的具體實(shí)現(xiàn)類(lèi)的 “全限定名”

    • 將spi所在jar放在主程序的classpath中

    • 服務(wù)調(diào)用方用java.util.ServiceLoader,用服務(wù)接口為參數(shù),去動(dòng)態(tài)加載具體的實(shí)現(xiàn)類(lèi)到JVM中

    案例展示

    案例業(yè)務(wù)背景:

    • 提供一個(gè)統(tǒng)一的支付接口

    • 有兩種支付方式,分別為支付寶支付和微信支付,實(shí)際中為不同支付廠商提供的SDK

    • 客戶(hù)端為customer工程,即調(diào)用支付SDK的使用者

    java中的spi怎么用

    從工程的結(jié)構(gòu)來(lái)看,也是遵循SPI的服務(wù)規(guī)范的,即在resources目錄下,創(chuàng)建一個(gè)指定名稱(chēng)的文件夾,將接口實(shí)現(xiàn)的全限定名放進(jìn)去,那么客戶(hù)端只需要依賴(lài)特定的SDK,然后通過(guò) serviceLoader的方式即可加載到依賴(lài)的SDK的服務(wù)

    客戶(hù)端customer工程導(dǎo)入依賴(lài)

    	<dependencies>
    
            <dependency>
                <artifactId>service-common</artifactId>
                <groupId>com.congge</groupId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <artifactId>ali-pay</artifactId>
                <groupId>com.congge</groupId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <artifactId>wechat-pay</artifactId>
                <groupId>com.congge</groupId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
        </dependencies>
    public class MainTest {
    
        public static void main(String[] args) {
    
            ServiceLoader<PayService> loader = ServiceLoader.load(PayService.class);
            loader.forEach(payService ->{
                System.out.println(payService);
                payService.pay();
                System.out.println("=======");
            });
        }
    
    }

    運(yùn)行下這段客戶(hù)端的測(cè)試程序

    java中的spi怎么用

    我們不妨來(lái)看看serviceLoader中的一段關(guān)鍵代碼,即加載服務(wù)接口時(shí),可以發(fā)現(xiàn),該方法最終要去找接口的實(shí)現(xiàn)類(lèi)所在jar包下的 “META-INF/services” 目錄中的服務(wù)實(shí)現(xiàn),如果找到了就能被加載和使用

    java中的spi怎么用

    java中的spi怎么用

    SPI優(yōu)點(diǎn)

    • 使用Java SPI機(jī)制的優(yōu)勢(shì)是實(shí)現(xiàn)解耦,使得第三方服務(wù)模塊的裝配控制的邏輯與調(diào)用者的業(yè)務(wù)代碼分離

    • 應(yīng)用程序可根據(jù)實(shí)際業(yè)務(wù)情況啟用框架擴(kuò)展或替換框架組件

    SPI缺點(diǎn)

    • srviceLoader 只能通過(guò)遍歷全部獲取,也就是接口的實(shí)現(xiàn)類(lèi)全部加載并實(shí)例化一遍

    • 如果并不想用某些實(shí)現(xiàn)類(lèi),它也被加載并實(shí)例化了,這就造成了浪費(fèi)

    • 獲取某個(gè)實(shí)現(xiàn)類(lèi)的方式不夠靈活,只能通過(guò)Iterator形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)

    • 多個(gè)并發(fā)多線程使用ServiceLoader類(lèi)的實(shí)例是不安全的,需要加鎖控制

    SPI機(jī)制在實(shí)際生產(chǎn)中的一個(gè)應(yīng)用

    在小編的實(shí)際項(xiàng)目開(kāi)發(fā)中,有這樣一個(gè)需求,標(biāo)準(zhǔn)產(chǎn)品針對(duì)單點(diǎn)登錄提供了多種實(shí)現(xiàn),比如 基于cas方案,ldap方案,oauth3.0方案等,針對(duì)每種方案,提供了一套具體的實(shí)現(xiàn),即封裝成了各自的jar包

    標(biāo)準(zhǔn)產(chǎn)品在出廠并在客戶(hù)端安裝的時(shí)候,會(huì)有一套默認(rèn)的實(shí)現(xiàn),即oauth3.0實(shí)現(xiàn),但是客戶(hù)方有時(shí)候有自己的一套,比如cas服務(wù)器,那么客戶(hù)希望能夠?qū)觕as單點(diǎn)登錄,這么以來(lái),具體到項(xiàng)目在實(shí)際部署的時(shí)候,就需要現(xiàn)場(chǎng)做一些特定的參數(shù)配置,將標(biāo)準(zhǔn)實(shí)現(xiàn)切換為 cas的實(shí)現(xiàn)即可,那么問(wèn)題來(lái)了,標(biāo)準(zhǔn)產(chǎn)品是如何根據(jù)參數(shù)配置做到的呢?

    其實(shí)也很簡(jiǎn)單,就是使用了 serviceLoader機(jī)制,自動(dòng)發(fā)現(xiàn)標(biāo)準(zhǔn)產(chǎn)品中能夠加載到的所有單點(diǎn)登錄實(shí)現(xiàn),如果沒(méi)有外部配置參數(shù)傳入,則默認(rèn)使用oauth3.0的實(shí)現(xiàn),否則,將會(huì)采用外部參數(shù)傳過(guò)來(lái)的那個(gè)實(shí)現(xiàn)。

    二、DUbbo 中SPI的使用

    可以說(shuō),dubbo框架是對(duì)spi使用的一個(gè)很好的例子,dubbo框架本身就是基于SPI規(guī)范做了更進(jìn)一步的封裝,從上面的優(yōu)缺點(diǎn)分析中,我媽了解了原生的SPI在客戶(hù)端選擇服務(wù)的時(shí)候需要遍歷所有的接口實(shí)現(xiàn),比較浪費(fèi)資源,而dubbo在此基礎(chǔ)上有了更好的封裝和實(shí)現(xiàn),下面來(lái)了解下dubbo的SPI使用吧

    Dubbo 的 SPI 規(guī)范是:

    接口名:可隨意定義

    實(shí)現(xiàn)類(lèi)名:在接口名前添加一個(gè)用于表示自身功能的“標(biāo)識(shí)前輟”字符串

    提供者配置文件路徑:在依次查找的目錄為

    • META-INF/dubbo/internal

    • META-INF/dubbo

    • META-INF/services

    提供者配置文件名稱(chēng):接口的全限定性類(lèi)名,無(wú)需擴(kuò)展名

    提供者配置文件內(nèi)容:文件的內(nèi)容為 key=value 形式,value 為該接口的實(shí)現(xiàn)類(lèi)的全限類(lèi)性類(lèi)名,key 可以隨意,但一般為該實(shí)現(xiàn)類(lèi)的“標(biāo)識(shí)前輟”(首字母小寫(xiě))。一個(gè)類(lèi)名占 一行

    提供者加載:ExtensionLoader 類(lèi)相當(dāng)于 JDK SPI 中的 ServiceLoader 類(lèi),用于加載提供者配置文件中所有的實(shí)現(xiàn)類(lèi),并創(chuàng)建相應(yīng)的實(shí)例

    Dubbo 的 SPI 舉例

    1、創(chuàng)建一個(gè)maven工程,并導(dǎo)入核心依賴(lài)

    		<dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo</artifactId>
                <version>3.0.0</version>
            </dependency>
    
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
            </dependency>

    2、 定義 SPI 接口

    比如這里有一個(gè)下單的接口,注意接口上需要加上@SPI 注解 ,注解里面的值可以填,也可以不用填,如果填,請(qǐng)注意和配置文件里面的key值名稱(chēng)保持一致,填寫(xiě)了的話(huà),加載的時(shí)候,會(huì)默認(rèn)找這個(gè)key對(duì)應(yīng)的實(shí)現(xiàn)

    @SPI("alipay")
    public interface Order {
        String way();
    }

    3、定義兩個(gè)接口的實(shí)現(xiàn)類(lèi)

    public class AlipayOrder implements Order{
    
        public String way() {
            System.out.println("使用支付寶支付");
            return "支付寶支付";
        }
    
    }
    public class WechatOrder implements Order {
    
        public String way() {
            System.out.println("微信支付");
            return "微信支付";
        }
    
    }

    4、定義擴(kuò)展類(lèi)配置文件

    java中的spi怎么用

    alipay=com.congge.spi.AlipayOrder
    wechat=com.congge.spi.WechatOrder

    5、測(cè)試方法

    	@Test
        public void test1(){
            ExtensionLoader<Order> extensionLoader = ExtensionLoader.getExtensionLoader(Order.class);
            Order alipay = extensionLoader.getExtension("alipay");
            System.out.println(alipay.way());
    
            Order wechat = extensionLoader.getExtension("wechat");
            System.out.println(wechat.way());
        }

    java中的spi怎么用

    如果不指定加載哪個(gè),而且接口配置了默認(rèn)值,這里只需要在getExtension中設(shè)置 “true”,就會(huì)自動(dòng)加載默認(rèn)的那個(gè)

    java中的spi怎么用

    在Dubbo源碼中,很多地方會(huì)存在下面這樣的三種代碼,分別是自適應(yīng)擴(kuò)展點(diǎn)、指定名稱(chēng)的擴(kuò)展點(diǎn)、激活擴(kuò)展點(diǎn),dubbo通過(guò)這些擴(kuò)展的spi接口實(shí)現(xiàn)眾多的插拔式功能

    ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
    ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
    ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

    以dubbo源碼中的Protocol 為例,對(duì)應(yīng)dubbo源碼中的rpc模塊

    @SPI("dubbo")  
    public interface Protocol {  
          
        int getDefaultPort();  
      
        @Adaptive  
        <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  
      
        @Adaptive  
        <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  
    
        void destroy();  
     
    }

    java中的spi怎么用

    Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
    • Protocol接口,在運(yùn)行的時(shí)候dubbo會(huì)判斷一下應(yīng)該選用這個(gè)Protocol接口的哪個(gè)實(shí)現(xiàn)類(lèi)來(lái)實(shí)例化對(duì)象

    • 如果你配置了Protocol,則會(huì)將你配置的Protocol實(shí)現(xiàn)類(lèi)加載到JVM中來(lái),然后實(shí)例化對(duì)象時(shí),就用你配置的那個(gè)Protocol實(shí)現(xiàn)類(lèi)就可以了

    上面那行代碼就是dubbo里面大量使用的,就是對(duì)很多組件,都是保留一個(gè)接口和多個(gè)實(shí)現(xiàn),然后在系統(tǒng)運(yùn)行的時(shí)候動(dòng)態(tài)的根據(jù)配置去找到對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)。如果你沒(méi)有配置,那就走默認(rèn)的實(shí)現(xiàn)類(lèi),即dubbo

    三、springboot 中SPI思想的使用

    我們知道,springboot框架相比spring,從配置文件上簡(jiǎn)化了不少,但簡(jiǎn)化的只是開(kāi)發(fā)者看到的那些xml配置文件中的東西,其本質(zhì)仍然未變,就算是少了xml配置文件,依舊在啟動(dòng)的時(shí)候,需要做配置的解析工作,如解析原來(lái)的數(shù)據(jù)庫(kù)連接的xml配置文件中的內(nèi)容加載到spring容器中

    而springboot來(lái)說(shuō),很多看不到的配置文件,都是在容器啟動(dòng)過(guò)程中,自動(dòng)將配置進(jìn)行讀取,解析和加載,而在這個(gè)過(guò)程中,我們不禁好奇,這些配置是存在哪里呢?這里就用到了SPI的思想,也就是涉及到springboot的自動(dòng)裝配過(guò)程

    舉例來(lái)說(shuō),springboot怎么知道啟動(dòng)時(shí)需要加載 DataSource這個(gè)數(shù)據(jù)庫(kù)連接的bean對(duì)象呢?怎么知道要使用JdbcTemplate 還是Druid的連接呢?

    在spingboot工程啟動(dòng)過(guò)程中,有很重要的一個(gè)工作,就是完成bean的自動(dòng)裝配過(guò)程,自動(dòng)裝配裝配的是什么東西呢?簡(jiǎn)單來(lái)說(shuō)就是:

    • 掃描classpath(工程目錄下)下所有依賴(lài)的jar包裝中指定目錄中以特定的全限定名稱(chēng)的文件,進(jìn)行解析并裝配成bean

    • 掃描xml文件,解析xml配置并裝配成bean

    • 解析那些被認(rèn)為是需要裝配的配置類(lèi),如@configuration,@service等

    其中第一步中的那些文件是什么呢?其實(shí)就是和dubbo或原生的spi規(guī)范中的那些 /META-INF 文件,只不過(guò)在springboot工程中,命名的格式和規(guī)范稍有不同

    下面通過(guò)源碼來(lái)看看springboot啟動(dòng)過(guò)程中是如何加載這些spi文件的吧

    java中的spi怎么用

    然后來(lái)到下面這里,重點(diǎn)關(guān)注setInitializers 這個(gè)方法,顧名思義,表示在啟動(dòng)過(guò)程中要做的一些初始化設(shè)置,那么要設(shè)置哪些東西呢?

    java中的spi怎么用

    在這個(gè)方法中,有一個(gè)方法getSpringFactoriesInstances,緊接著這個(gè)方法看進(jìn)去

    java中的spi怎么用

    在該方法中需要重點(diǎn)關(guān)注這句代碼,通過(guò)這句代碼,將依賴(lài)包下的那些待裝配的文件進(jìn)行加載,說(shuō)白了,就是加載classpath下的那些 spring.factory的文件里面的name

    SpringFactoriesLoader.loadFactoryNames(type, classLoader)

    那么問(wèn)題是具體加載的是什么樣的文件呢?不妨繼續(xù)點(diǎn)進(jìn)去看看,在SpringFactoriesLoader類(lèi)的開(kāi)頭,有一個(gè)這樣的路徑,想必大家就猜到是什么了吧

    java中的spi怎么用

    也就是說(shuō),會(huì)去找以這樣的名字結(jié)尾的文件,比如我們?cè)谝蕾?lài)的jar包中,看到下面這一幕,在這個(gè)spring.factories中,會(huì)看到更多我們熟悉的配置

    java中的spi怎么用

    java中的spi怎么用

    這樣問(wèn)題就很明白了,通過(guò)找到spring.factories的文件,然后解析出具體的類(lèi)的完整的名稱(chēng),然后再在:createSpringFactoriesInstances 這個(gè)方法中完成對(duì)這些 擴(kuò)展的SPI接口實(shí)現(xiàn)類(lèi)的初始化加載,即完成配的過(guò)程

    java中的spi怎么用

    感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“java中的spi怎么用”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來(lái)學(xué)習(xí)!

    向AI問(wèn)一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI