一、什麼是SPI ?
SPI 全稱:Service Provider Interface,是Java提供的一套用來被第三方實作或者擴充的接口,它可以用來啟用架構擴充和替換元件。
面向的對象的設計裡,我們一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。一旦代碼裡涉及具體的實作類,就違反了可拔插的原則,如果需要替換一種實作,就需要修改代碼。
為了實作在子產品裝配的時候不用在程式裡動态指明,這就需要一種服務發現機制。java spi就是提供這樣的一個機制:為某個接口尋找服務實作的機制。這有點類似IOC的思想,将裝配的控制權移到了程式之外。
SPI的作用就是為被擴充的API尋找服務實作。
SPI(Service Provider Interface),是JDK内置的一種 服務提供發現機制,可以用來啟用架構擴充和替換元件,主要是被架構的開發人員使用,比如java.sql.Driver接口,其他不同廠商可以針對同一接口做出不同的實作,MySQL和PostgreSQL都有不同的實作提供給使用者,而Java的SPI機制可以為某個接口尋找服務實作。Java中SPI機制主要思想是将裝配的控制權移到程式之外,在子產品化設計中這個機制尤其重要,其核心思想就是 解耦。
本質:Java SPI 實際上是“基于接口的程式設計+政策模式+約定配置檔案” 組合實作的動态加載機制,在JDK中提供了工具類:“java.util.ServiceLoader”來實作服務查找。
SPI整體機制圖如下:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iNihzY1IGMxUTN4UDO0QmZ3gTY3cDN2cDMjNzMwQmY58CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
當服務的提供者提供了一種接口的實作之後,需要在classpath下的META-INF/services/目錄裡建立一個以服務接口命名的檔案,這個檔案裡的内容就是這個接口的具體的實作類。當其他的程式需要這個服務的時候,就可以通過查找這個jar包(一般都是以jar包做依賴)的META-INF/services/中的配置檔案,配置檔案中有接口的具體實作類名,可以根據這個類名進行加載執行個體化,就可以使用該服務了。JDK中查找服務的實作的工具類是:java.util.ServiceLoader。
二、SPI 的不足
1.不能按需加載,需要周遊所有的實作,并執行個體化,然後在循環中才能找到我們需要的實作。如果不想用某些實作類,或者某些類執行個體化很耗時,它也被載入并執行個體化了,這就造成了浪費。
2.擷取某個實作類的方式不夠靈活,隻能通過 Iterator 形式擷取,不能根據某個參數來擷取對應的實作類。(Spring 的BeanFactory,ApplicationContext 就要進階一些了。)
3.多個并發多線程使用 ServiceLoader 類的執行個體是不安全的。
三、API 與 SPI
API代表應用程式程式設計接口,其中API是一種通路某種軟體或平台提供的服務/功能的方法。
SPI代表服務提供商接口,其中SPI是注入,擴充或更改軟體或平台行為的方式。
API通常是用作用戶端通路服務,并且具有以下屬性:
1、API是一種通路服務以實作特定行為或輸出的程式設計方式。
2、但是API一旦被用戶端使用,除非有适當的通信,否則它不能(也不應)被更改/删除。
SPI面向服務提供者,并且具有以下屬性:
1、SPI是一種擴充/更改軟體或平台行為的方式。
2、添加SPI接口将導緻問題并可能破壞現有的實作。
換句話說,API會告訴您特定的類/方法為您執行什麼操作,而SPI則告訴您必須執行哪些操作才能符合要求。
API (Application Programming Interface)在大多數情況下,都是實作方制定接口并完成對接口的實作,調用方僅僅依賴接口調用,且無權選擇不同實作。 從使用人員上來說,API 直接被應用開發人員使用。
SPI (Service Provider Interface)是調用方來制定接口規範,提供給外部來實作,調用方在調用時則選擇自己需要的外部實作。 從使用人員上來說,SPI 被架構擴充人員使用。
四、SPI 應用場景
SPI擴充機制應用場景有很多,比如Common-Logging,JDBC,Dubbo等等。
SPI流程:
- 有關組織和公式定義接口标準
- 第三方提供具體實作: 實作具體方法, 配置 META-INF/services/${interface_name} 檔案
- 開發者使用
比如JDBC場景下:
首先在Java中定義了接口java.sql.Driver,并沒有具體的實作,具體的實作都是由不同廠商提供。
在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目錄,該目錄下會有一個名字為java.sql.Driver的檔案,檔案内容是com.mysql.cj.jdbc.Driver,這裡面的内容就是針對Java中定義的接口的實作。
同樣在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同樣的配置檔案,檔案内容是org.postgresql.Driver,這是PostgreSQL對Java的java.sql.Driver的實作。
個人簡單描述下使用的過程(源碼就不貼了):
- 建立連結程式提前執行ServiceLoader對java.sql.Driver的具體實作進行執行個體化。
- 具體的執行個體化中會擷取目前服務的一些配置屬性,封裝完善該Driver具體實作類。
- 建立連結的方法使用到具體的Driver實作類建立連結。
五、ServiceLoader原理
服務端很好了解,就是一個定義,用戶端來看看ServiceLoader的源碼。
首先,ServiceLoader實作了Iterable接口,是以它有疊代器的屬性,這裡主要都是實作了疊代器的hasNext和next方法。這裡主要都是調用的lookupIterator的相應hasNext和next方法,lookupIterator是懶加載疊代器。
其次,LazyIterator中的hasNext方法,靜态變量PREFIX就是”META-INF/services/”目錄,這也就是為什麼需要在classpath下的META-INF/services/目錄裡建立一個以服務接口命名的檔案。
最後,通過反射方法Class.forName()加載類對象,并用newInstance方法将類執行個體化,并把執行個體化後的類緩存到providers對象中,(LinkedHashMap<String,S>類型) 然後傳回執行個體對象。
六、代碼實踐案例
1、利用三方jar包來利用注解生成META-INFO/services, 注解:@MetaInfServices
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
</dependency>
定義用戶端為interface工程,定義學習接口Study,定義方法為學習方式
定義服務端是provider工程,定義三個實作類為配置元件,加入注解 @MetaInfServices
編譯之後target存在配置檔案
2、直接使用架構的SPI機制(Spring SPI)
Spring SPI介紹:
- 工具類:Spring中使用的類是SpringFactoriesLoader,在org.springframework.core.io.support包中
- SpringFactoriesLoader 會掃描 classpath 中的 META-INF/spring.factories檔案。
- SpringFactoriesLoader 會加載并執行個體化 META-INF/spring.factories 中的指定類型。
- META-INF/spring.factories 内容必須是 properties 的Key-Value形式,多值以逗号隔開。
SPI技術了解及應用
使用細節:
- spring.factories内容的key不隻能是接口,也可以是抽象類、具體的類。但是有個原則:=後面必須是key的實作類(子類)
- key還可以是注解,比如SpringBoot中的的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,它就是一個注解。
- 檔案的格式需要保證正确,否則會傳回[](不會報錯)
- =右邊必須不是抽象類,必須能夠執行個體化。且有空的構造函數~
- loadFactories依賴方法loadFactoryNames。loadFactoryNames方法隻拿全類名,loadFactories拿到全類名後會立馬執行個體化。
- 此處特别注意:loadFactories執行個體化完成所有執行個體後,會調用AnnotationAwareOrderComparator.sort(result)排序,是以它是支援Ordered接口排序的,這個特點特别的重要。
更多精彩文章請通路我的個人部落格(zhuoerhuobi.cn)