關於SPI的定義參考博客:https://blog.csdn.net/gallenzhang/article/details/88958800
1. 什么是SPI
SPI是java內置的一種服務發現機制,一般在框架設計的時候,將問題抽象成接口,至於服務的實現,由不同的廠家來各自實現,按照指定的規范引入哪個廠家的實現jar包,就可以使用該廠家的服務實現,這種現象個人理解跟java的多態很類似。
2. 為什么要使用SPI
假設現在沒有使用SPI,存在一個接口,和不同的實現類,然后在使用該接口的時候,需要硬編碼的形式使用哪個實現類,例如使用工廠模式+策略模式,根據指定條件硬編碼,如果我們新增加一種實現,還是需要修改工廠模式的編碼,所以跟業務代碼存在耦合。而使用SPI機制,可以將第三方的實現方式作為插件的方式,可插拔方式,可以很大程度的與業務代碼進行解耦。
但是,這種方式也有缺點,假設配置了多個實現類,不能很好的根據條件的來進行判斷篩選,只能針對Iterator遍歷獲取。
3. 關於策略模式和SPI的幾點區別
如果從代碼接入的級別來看,策略模式還是在原有項目中進行代碼修改,只不過它不會修改原有類中的代碼,而是新建了一個類。而 SPI 機制則是不會修改原有項目中的代碼,其會新建一個項目,最終以 Jar 包引入的方式代碼。
從這一點來看,無論策略模式還是 SPI 機制,他們都是將修改與原來的代碼隔離開來,從而避免新增代碼對原有代碼的影響。但策略模式是類層次上的隔離,而 SPI 機制則是項目框架級別的隔離。
從應用領域來說,策略模式更多應用在業務領域,即業務代碼書寫以及業務代碼重構。而 SPI 機制更多則是用於框架的設計領域,通過 SPI 機制提供的靈活性,讓框架擁有良好的插件特性,便於擴展。
- 從設計思想來看。策略模式和 SPI 機制其思想是類似的,都是通過一定的設計隔離變化的部分,從而讓原有部分更加穩定。
- 從隔離級別來看。策略模式的隔離是類級別的隔離,而 SPI 機制是項目級別的隔離。
- 從應用領域來看。策略模式更多用在業務代碼書寫,SPI 機制更多用於框架的設計。
4. 使用介紹或者說約定
4.1 首先介紹幾個名詞
- 服務提供者:接口服務提供者,規則定義者,通俗來說,負責寫接口
- 服務消費者:接口服務實現對象,具體的規則實現。
- 調用方:具體的業務工程,例如在一個Main方法中使用該接口的所在類所在工程。
4.2 約定
- 定義一個接口
- 服務消費者所在工程里,在根路徑下(針對web項目,可以理解成resources目錄下,非web項目,就是src目錄下),創建一個文件夾,即META-INF/services, 然后創建一個文件,文件名就是定義的接口的全類名,文件內容就是當前的服務消費者所在工程里針對該接口的實現類對應的全類名。
- 調用方使用 java.util.ServiceLoader 類來加載服務消費者。
5. 具體的demo實現
在這個demo中,不管是服務提供者還是消費者,以及調用者都是在不同的工程中,不要全部都放在一個工程里,這樣比較符合規范和設計,以及能夠比較清楚的理解其中的使用細節。
由於是demo,這里就提供一個計算接口,由不同的實現來實現數據的計算,輸出結果。
5.1 創建服務提供者
這里創建一個工程 spi-interface,里面專門提供服務接口。
5.1.1 接口代碼如下:
package services;
/**
* @author ztkj-hzb
* @Date 2020/1/12 14:00
* @Description
*/
public interface ICalculationService {
/**
* 數學計算
*
* @param param1
* @param param2
* @return
*/
Object calc(Object param1, Object param2);
}
項目結構如下圖:
5.1.2 將該項目打包成jar包
如果不會使用idea或者eclipse打包的,可以百度一下,這里提供一下關於idea打包的參考博客:https://www.cnblogs.com/flyToDreamJava/p/8991201.html
5.2 創建服務消費者A
新建一個工程 spi-impl1,在這里實現了計算接口,使用加法進行數據的運算。注意:這里還需要引入剛才服務提供者的打包好的jar包,不然找不到需要實現的接口。
5.2.1 實現接口
package service.impl;
import services.ICalculationService;
/**
* @author ztkj-hzb
* @Date 2020/1/12 14:06
* @Description
*/
public class CalculationServiceImplB implements ICalculationService {
/**
* 加法運算
* @param o
* @param o1
* @return
*/
@Override
public Object calc(Object o, Object o1) {
return (Double)o + (Double)o1;
}
}
5.2.2 按照約定,創建約定目錄以及文件
創建目錄 META-INF/services,從上述的服務提供者結構圖來看,我們得知接口的全類名是 "services.ICalculationService" , 所以我們需要創建一個文件,名字就是我們的全類名,里面的內容,就是我們剛才的實現類的全類名。
具體的項目結構如下圖:
5.2.3 將該工程打包
5.3 創建服務消費者B
服務消費者B項目結構跟A幾乎一致,本不想重復寫,考慮有些朋友跟我一樣是新手,所以就多寫一點。
新建一個工程 spi-impl2,在這里實現了計算接口,使用減法進行數據的運算。再次提醒注意:這里還需要引入剛才服務提供者的打包好的jar包,不然找不到需要實現的接口。
5.3.1 實現接口
package service.impl;
import services.ICalculationService;
/**
* @author ztkj-hzb
* @Date 2020/1/12 14:08
* @Description
*/
public class CalculationServiceImplA implements ICalculationService {
/**
* 減法運算
*
* @param o
* @param o1
* @return
*/
@Override
public Object calc(Object o, Object o1) {
return (Double) o - (Double) o1;
}
}
5.3.2 按照約定,創建約定目錄以及文件
創建目錄 META-INF/services,從上述的服務提供者結構圖來看,我們得知接口的全類名是 "services.ICalculationService" , 所以我們需要創建一個文件,名字就是我們的全類名,里面的內容,就是我們剛才的實現類的全類名。
具體的項目結構如下圖:
5.3.3 將該工程打包
5.4 創建調用者
創建一個工程 spi-main, 注意:需要引用上面三個項目的jar包。
5.4.1 編寫測試調用方法
package com.lonely;
import services.ICalculationService;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @author ztkj-hzb
* @Date 2020/1/12 14:18
* @Description
*/
public class Main {
public static void main(String[] args) {
ServiceLoader<ICalculationService> serviceLoader = ServiceLoader.load(ICalculationService.class);
Iterator<ICalculationService> iterator = serviceLoader.iterator();
while(iterator.hasNext()){
ICalculationService calculationService = iterator.next();
System.out.println(MessageFormat.format("指定類:{0}.calc()執行結果是:{1}",calculationService.getClass().getName(),calculationService.calc(23.1,15.3)));
}
}
}
執行結果如下:
指定類:service.impl.CalculationServiceImplA.calc()執行結果是:7.8
指定類:service.impl.CalculationServiceImplB.calc()執行結果是:38.4
從結果上看,我們在調用方工程代碼中,沒有寫上一行關於 "CalculationServiceImplA"或者"CalculationServiceImplB"相關服務的硬代碼,只要后續新添加一種實現,只需要在編寫一個工程,按照SPI約定,打包成jar后,放入到調用者工程中,就能使用了,完全與具體實現解耦。