結合實戰和原始碼來聊聊Java中的SPI機制?
阿新 • • 發佈:2020-11-20
## 寫在前面
> 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