java spi最全使用總結

前言

在開發過程中,經常要用到第三方提供的SDK來完成一些業務擴展功能,比如調用第三方的發短信、圖片驗證碼、人臉識別等等功能,但問題是,第三方SDK隻是提供瞭標準的功能實現,某些場景下,開發者還想基於這些SDK做一些個性化的定制和擴展,那要怎麼辦呢?

於是,一些優秀的SDK就通過SPI的機制,將一些接口擴能開放出來,開發者就可以基於這些SPI接口做自身的業務擴展瞭;

總結一下SPI思想:在系統的各個模塊中,往往有不同的實現方案,例如日志模塊的方案、xml解析的方案等,為瞭在裝載模塊的時候不具體指明實現類,我們需要一種服務發現機制,java spi就提供這樣一種機制。有點類似於IoC的思想,將服務裝配的控制權移到程序之外,在模塊化設計時尤其重要

Java 的SPI機制在很多框架,中間件等都有著廣泛的使用,如springboot,Dubbo中均有采用,屬於高級Java開發知識點,有必要掌握

下面用一張簡圖說明下SPI機制的原理

一、JDK中SPI的使用規范

  • 定義通用的服務接口,針對服務接口,提供具體實現類
  • 在jar包的META-INF/services/目錄中,新建一個文件,文件名為 接口的 “全限定名”, 文件內容為該接口的具體實現類的 “全限定名”
  • 將spi所在jar放在主程序的classpath中
  • 服務調用方用java.util.ServiceLoader,用服務接口為參數,去動態加載具體的實現類到JVM中

案例展示

案例業務背景:

  • 提供一個統一的支付接口
  • 有兩種支付方式,分別為支付寶支付和微信支付,實際中為不同支付廠商提供的SDK
  • 客戶端為customer工程,即調用支付SDK的使用者

從工程的結構來看,也是遵循SPI的服務規范的,即在resources目錄下,創建一個指定名稱的文件夾,將接口實現的全限定名放進去,那麼客戶端隻需要依賴特定的SDK,然後通過 serviceLoader的方式即可加載到依賴的SDK的服務

客戶端customer工程導入依賴

	<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("=======");
        });
    }

}

運行下這段客戶端的測試程序

我們不妨來看看serviceLoader中的一段關鍵代碼,即加載服務接口時,可以發現,該方法最終要去找接口的實現類所在jar包下的 “META-INF/services” 目錄中的服務實現,如果找到瞭就能被加載和使用

SPI優點

  • 使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離
  • 應用程序可根據實際業務情況啟用框架擴展或替換框架組件

SPI缺點

  • srviceLoader 隻能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍
  • 如果並不想用某些實現類,它也被加載並實例化瞭,這就造成瞭浪費
  • 獲取某個實現類的方式不夠靈活,隻能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類
  • 多個並發多線程使用ServiceLoader類的實例是不安全的,需要加鎖控制

SPI機制在實際生產中的一個應用

在小編的實際項目開發中,有這樣一個需求,標準產品針對單點登錄提供瞭多種實現,比如 基於cas方案,ldap方案,oauth2.0方案等,針對每種方案,提供瞭一套具體的實現,即封裝成瞭各自的jar包

標準產品在出廠並在客戶端安裝的時候,會有一套默認的實現,即oauth2.0實現,但是客戶方有時候有自己的一套,比如cas服務器,那麼客戶希望能夠對接cas單點登錄,這麼以來,具體到項目在實際部署的時候,就需要現場做一些特定的參數配置,將標準實現切換為 cas的實現即可,那麼問題來瞭,標準產品是如何根據參數配置做到的呢?

其實也很簡單,就是使用瞭 serviceLoader機制,自動發現標準產品中能夠加載到的所有單點登錄實現,如果沒有外部配置參數傳入,則默認使用oauth2.0的實現,否則,將會采用外部參數傳過來的那個實現。

二、DUbbo 中SPI的使用

可以說,dubbo框架是對spi使用的一個很好的例子,dubbo框架本身就是基於SPI規范做瞭更進一步的封裝,從上面的優缺點分析中,我媽瞭解瞭原生的SPI在客戶端選擇服務的時候需要遍歷所有的接口實現,比較浪費資源,而dubbo在此基礎上有瞭更好的封裝和實現,下面來瞭解下dubbo的SPI使用吧

Dubbo 的 SPI 規范是:

接口名:可隨意定義

實現類名:在接口名前添加一個用於表示自身功能的“標識前輟”字符串

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

  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

提供者配置文件名稱:接口的全限定性類名,無需擴展名

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

提供者加載:ExtensionLoader 類相當於 JDK SPI 中的 ServiceLoader 類,用於加載提供者配置文件中所有的實現類,並創建相應的實例

Dubbo 的 SPI 舉例

1、創建一個maven工程,並導入核心依賴

		<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 接口

比如這裡有一個下單的接口,註意接口上需要加上@SPI 註解 ,註解裡面的值可以填,也可以不用填,如果填,請註意和配置文件裡面的key值名稱保持一致,填寫瞭的話,加載的時候,會默認找這個key對應的實現

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

3、定義兩個接口的實現類

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、定義擴展類配置文件

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

5、測試方法

	@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());
    }

如果不指定加載哪個,而且接口配置瞭默認值,這裡隻需要在getExtension中設置 “true”,就會自動加載默認的那個

在Dubbo源碼中,很多地方會存在下面這樣的三種代碼,分別是自適應擴展點、指定名稱的擴展點、激活擴展點,dubbo通過這些擴展的spi接口實現眾多的插拔式功能

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

以dubbo源碼中的Protocol 為例,對應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();  
 
} 

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
  • Protocol接口,在運行的時候dubbo會判斷一下應該選用這個Protocol接口的哪個實現類來實例化對象
  • 如果你配置瞭Protocol,則會將你配置的Protocol實現類加載到JVM中來,然後實例化對象時,就用你配置的那個Protocol實現類就可以瞭

上面那行代碼就是dubbo裡面大量使用的,就是對很多組件,都是保留一個接口和多個實現,然後在系統運行的時候動態的根據配置去找到對應的實現類。如果你沒有配置,那就走默認的實現類,即dubbo

三、springboot 中SPI思想的使用

我們知道,springboot框架相比spring,從配置文件上簡化瞭不少,但簡化的隻是開發者看到的那些xml配置文件中的東西,其本質仍然未變,就算是少瞭xml配置文件,依舊在啟動的時候,需要做配置的解析工作,如解析原來的數據庫連接的xml配置文件中的內容加載到spring容器中

而springboot來說,很多看不到的配置文件,都是在容器啟動過程中,自動將配置進行讀取,解析和加載,而在這個過程中,我們不禁好奇,這些配置是存在哪裡呢?這裡就用到瞭SPI的思想,也就是涉及到springboot的自動裝配過程

舉例來說,springboot怎麼知道啟動時需要加載 DataSource這個數據庫連接的bean對象呢?怎麼知道要使用JdbcTemplate 還是Druid的連接呢?

在spingboot工程啟動過程中,有很重要的一個工作,就是完成bean的自動裝配過程,自動裝配裝配的是什麼東西呢?簡單來說就是:

  • 掃描classpath(工程目錄下)下所有依賴的jar包裝中指定目錄中以特定的全限定名稱的文件,進行解析並裝配成bean
  • 掃描xml文件,解析xml配置並裝配成bean
  • 解析那些被認為是需要裝配的配置類,如@configuration,@service等

其中第一步中的那些文件是什麼呢?其實就是和dubbo或原生的spi規范中的那些 /META-INF 文件,隻不過在springboot工程中,命名的格式和規范稍有不同

下面通過源碼來看看springboot啟動過程中是如何加載這些spi文件的吧

然後來到下面這裡,重點關註setInitializers 這個方法,顧名思義,表示在啟動過程中要做的一些初始化設置,那麼要設置哪些東西呢?

在這個方法中,有一個方法getSpringFactoriesInstances,緊接著這個方法看進去

在該方法中需要重點關註這句代碼,通過這句代碼,將依賴包下的那些待裝配的文件進行加載,說白瞭,就是加載classpath下的那些 spring.factory的文件裡面的name

SpringFactoriesLoader.loadFactoryNames(type, classLoader)

那麼問題是具體加載的是什麼樣的文件呢?不妨繼續點進去看看,在SpringFactoriesLoader類的開頭,有一個這樣的路徑,想必大傢就猜到是什麼瞭吧

也就是說,會去找以這樣的名字結尾的文件,比如我們在依賴的jar包中,看到下面這一幕,在這個spring.factories中,會看到更多我們熟悉的配置

這樣問題就很明白瞭,通過找到spring.factories的文件,然後解析出具體的類的完整的名稱,然後再在:createSpringFactoriesInstances 這個方法中完成對這些 擴展的SPI接口實現類的初始化加載,即完成配的過程

沿著這個思路繼續探究下去,相信感興趣的同學對springboot中的這種類SPI的方式會有更深一層的理解,本篇到此結束,最後感謝觀看!

到此這篇關於java spi最全使用總結的文章就介紹到這瞭,更多相關java spi詳解內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: