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!
推薦閱讀:
- Java Spring Dubbo三種SPI機制的區別
- SpringBoot詳細介紹SPI機制示例
- 淺析Java SPI 與 dubbo SPI
- 深入解析Spring Boot 的SPI機制詳情
- Java SPI機制詳細介紹