天天看點

SPI技術了解及應用

一、什麼是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整體機制圖如下:

SPI技術了解及應用

當服務的提供者提供了一種接口的實作之後,需要在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的實作。

個人簡單描述下使用的過程(源碼就不貼了):

  1. 建立連結程式提前執行ServiceLoader對java.sql.Driver的具體實作進行執行個體化。
  2. 具體的執行個體化中會擷取目前服務的一些配置屬性,封裝完善該Driver具體實作類。
  3. 建立連結的方法使用到具體的Driver實作類建立連結。

五、ServiceLoader原理

服務端很好了解,就是一個定義,用戶端來看看ServiceLoader的源碼。

SPI技術了解及應用

首先,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,定義方法為學習方式

SPI技術了解及應用

定義服務端是provider工程,定義三個實作類為配置元件,加入注解 @MetaInfServices

SPI技術了解及應用

編譯之後target存在配置檔案

SPI技術了解及應用

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)