1. 程式人生 > >結合實戰和原始碼來聊聊Java中的SPI機制?

結合實戰和原始碼來聊聊Java中的SPI機制?

## 寫在前面 > SPI機制能夠非常方便的為某個介面動態指定其實現類,在某種程度上,這也是某些框架具有高度可擴充套件性的基礎。今天,我們就從原始碼級別深入探討下Java中的SPI機制。 > > 注:文章已收錄到:[https://github.com/sunshinelyz/technology-binghe](https://github.com/sunshinelyz/technology-binghe) ## SPI的概念 SPI在Java中的全稱為Service Provider Interface,是JDK內建的一種服務提供發現機制,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。 ```java JAVA SPI = 基於介面的程式設計+策略模式+配置檔案的動態載入機制 ``` ## SPI的使用場景 Java是一種面嚮物件語言,雖然Java8開始支援函數語言程式設計和Stream,但是總體來說,還是面向物件的語言。在使用Java進行面向物件開發時,一般會推薦使用基於介面的程式設計,程式的模組與模組之前不會直接進行實現類的硬編碼。而在實際的開發過程中,往往一個介面會有多個實現類,各實現類要麼實現的邏輯不同,要麼使用的方式不同,還有的就是實現的技術不同。為了使呼叫方在呼叫介面的時候,明確的知道自己呼叫的是介面的哪個實現類,或者說為了實現在模組裝配的時候不用在程式裡動態指明,這就需要一種服務發現機制。Java中的SPI載入機制能夠滿足這樣的需求,它能夠自動尋找某個介面的實現類。 大量的框架使用了Java的SPI技術,如下: (1)JDBC載入不同型別的資料庫驅動 (2)日誌門面介面實現類載入,SLF4J載入不同提供商的日誌實現類 (3)Spring中大量使用了SPI * 對servlet3.0規範 * 對ServletContainerInitializer的實現 * 自動型別轉換Type Conversion SPI(Converter SPI、Formatter SPI)等 (4)Dubbo裡面有很多個元件,每個元件在框架中都是以介面的形成抽象出來!具體的實現又分很多種,在程式執行時根據使用者的配置來按需取介面的實現 ## SPI的使用 當服務的提供者,提供了介面的一種實現後,需要在Jar包的META-INF/services/目錄下,建立一個以介面的名稱(包名.介面名的形式)命名的檔案,在檔案中配置介面的實現類(完整的包名+類名)。 當外部程式通過java.util.ServiceLoader類裝載這個介面時,就能夠通過該Jar包的META/Services/目錄裡的配置檔案找到具體的實現類名,裝載例項化,完成注入。同時,SPI的規範規定了介面的實現類必須有一個無參構造方法。 SPI中查詢介面的實現類是通過java.util.ServiceLoader,而在java.util.ServiceLoader類中有一行程式碼如下: ```java // 載入具體實現類資訊的字首,也就是以介面命名的檔案需要放到Jar包中的META-INF/services/目錄下 private static final String PREFIX = "META-INF/services/"; ``` 這也就是說,我們必須將介面的配置檔案寫到Jar包的META/Services/目錄下。 ## SPI例項 這裡,給出一個簡單的SPI使用例項,演示在Java程式中如何使用SPI動態載入介面的實現類。 注意:例項是基於Java8進行開發的。 ### 1.建立Maven專案 在IDEA中建立Maven專案spi-demo,如下: ![](https://img-blog.csdnimg.cn/20191101102102162.png) ### 2.編輯pom.xml ```xml ``` ### 3.建立類載入工具類 在io.binghe.spi.loader包下建立MyServiceLoader,MyServiceLoader類中直接呼叫JDK的ServiceLoader類載入Class。程式碼如下所示。 ```java package io.binghe.spi.loader; import java.util.ServiceLoader; /** * @author binghe * @version 1.0.0 * @description 類載入工具 */ public class MyServiceLoader { /** * 使用SPI機制載入所有的Class */ public static ServiceLoader loadAll(final Class clazz) { return ServiceLoader.load(clazz); } } ``` ### 4.建立介面 在io.binghe.spi.service包下建立介面MyService,作為測試介面,介面中只有一個方法,列印傳入的字串資訊。程式碼如下所示: ```java package io.binghe.spi.service; /** * @author binghe * @version 1.0.0 * @description 定義介面 */ public interface MyService { /** * 列印資訊 */ void print(String info); } ``` ### 5.建立介面的實現類 **(1)建立第一個實現類MyServiceA** 在io.binghe.spi.service.impl包下建立MyServiceA類,實現MyService介面。程式碼如下所示: ```java package io.binghe.spi.service.impl; import io.binghe.spi.service.MyService; /** * @author binghe * @version 1.0.0 * @description 介面的第一個實現 */ public class MyServiceA implements MyService { @Override public void print(String info) { System.out.println(MyServiceA.class.getName() + " print " + info); } } ``` **(2)建立第二個實現類MyServiceB** 在io.binghe.spi.service.impl包下建立MyServiceB類,實現MyService介面。程式碼如下所示: ```java package io.binghe.spi.service.impl; import io.binghe.spi.service.MyService; /** * @author binghe * @version 1.0.0 * @description 介面第二個實現 */ public class MyServiceB implements MyService { @Override public void print(String info) { System.out.println(MyServiceB.class.getName() + " print " + info); } } ``` ### 6.建立介面檔案 在專案的src/main/resources目錄下建立META/Services/目錄,在目錄中建立io.binghe.spi.service.MyService檔案,注意:檔案必須是介面MyService的全名,之後將實現MyService介面的類配置到檔案中,如下所示: ```java io.binghe.spi.service.impl.MyServiceA io.binghe.spi.service.impl.MyServiceB ``` ### 7.建立測試類 在專案的io.binghe.spi.main包下建立Main類,該類為測試程式的入口類,提供一個main()方法,在main()方法中呼叫ServiceLoader類載入MyService介面的實現類。並通過Java8的Stream將結果打印出來,如下所示: ```java package io.binghe.spi.main; import io.binghe.spi.loader.MyServiceLoader; import io.binghe.spi.service.MyService; import java.util.ServiceLoader; import java.util.stream.StreamSupport; /** * @author binghe * @version 1.0.0 * @description 測試的main方法 */ public class Main { public static void main(String[] args){ ServiceLoader