1. spi 是什麼
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。
系統設計的各個抽象,往往有很多不同的實現方案,在面向的物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了開閉原則,Java SPI就是為某個介面尋找服務實現的機制,Java Spi的核心思想就是解耦。
整體機制圖如下:
Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。
總結起來就是:呼叫者根據實際使用需要,啟用、擴充套件、或者替換框架的實現策略
2. 應用場景
-
資料庫驅動載入介面實現類的載入
JDBC載入不同型別資料庫的驅動
-
日誌門面介面實現類載入
SLF4J載入不同提供應商的日誌實現類
-
Spring Spring Boot
自動裝配過程中,載入META-INF/spring.factories檔案,解析properties檔案
-
Dubbo
Dubbo大量使用了SPI技術,裡面有很多個元件,每個元件在框架中都是以介面的形成抽象出來
例如Protocol 協議介面
3. 使用步驟
以支付服務為例:
-
建立一個
PayService
新增一個pay
方法package com.imooc.spi; import java.math.BigDecimal; public interface PayService { void pay(BigDecimal price); } 複製程式碼
-
建立
AlipayService
和WechatPayService
,實現PayService
⚠️SPI的實現類必須攜帶一個不帶引數的構造方法;
package com.imooc.spi; import java.math.BigDecimal; public class AlipayService implements PayService{ public void pay(BigDecimal price) { System.out.println("使用支付寶支付"); } } 複製程式碼
package com.imooc.spi; import java.math.BigDecimal; public class WechatPayService implements PayService{ public void pay(BigDecimal price) { System.out.println("使用微信支付"); } } 複製程式碼
-
resources目錄下建立目錄META-INF/services
-
在META-INF/services建立com.imooc.spi.PayService檔案
-
先以AlipayService為例:在com.imooc.spi.PayService新增com.imooc.spi.AlipayService的檔案內容
-
建立測試類
package com.imooc.spi; import com.sun.tools.javac.util.ServiceLoader; import java.math.BigDecimal; public class PayTests { public static void main(String[] args) { ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class); for (PayService payService : payServices) { payService.pay(new BigDecimal(1)); } } } 複製程式碼
-
執行測試類,檢視返回結果
使用支付寶支付 複製程式碼
4. 原理分析
首先,我們先開啟ServiceLoader<S>
這個類
public final class ServiceLoader<S> implements Iterable<S> {
// SPI檔案路徑的字首
private static final String PREFIX = "META-INF/services/";
// 需要載入類的介面
private Class<S> service;
// 類載入器
private ClassLoader loader;
// 快取providers,儲存著service實現
private LinkedHashMap<String, S> providers = new LinkedHashMap();
// 懶載入的查詢迭代器
private ServiceLoader<S>.LazyIterator lookupIterator;
......
}
複製程式碼
參考具體ServiceLoader具體原始碼,程式碼量不多,實現的流程如下:
-
應用程式呼叫ServiceLoader.load方法
// 1. 獲取ClassLoad public static <S> ServiceLoader<S> load(Class<S> var0) { ClassLoader var1 = Thread.currentThread().getContextClassLoader(); return load(var0, var1); } // 2. 呼叫構造方法 public static <S> ServiceLoader<S> load(Class<S> var0, ClassLoader var1) { return new ServiceLoader(var0, var1); } // 3. 校驗引數和ClassLoad private ServiceLoader(Class<S> var1, ClassLoader var2) { this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null"); this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2; this.reload(); } //4. 清理快取容器,例項懶載入迭代器 public void reload() { this.providers.clear(); this.lookupIterator = new ServiceLoader.LazyIterator(this.service, this.loader, null); } 複製程式碼
-
我們簡單看一下這個懶載入迭代器
private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; Enumeration<URL> configs; Iterator<String> pending; String nextName; private LazyIterator(Class<S> var1, ClassLoader var2) { this.configs = null; this.pending = null; this.nextName = null; this.service = var2; this.loader = var3; } // 迭代執行並獲取解析出來的com.imooc.spi.AlipayService public boolean hasNext() { if (this.nextName != null) { return true; } else { if (this.configs == null) { try { String var1 = "META-INF/services/" + this.service.getName(); if (this.loader == null) { this.configs = ClassLoader.getSystemResources(var1); } else { this.configs = this.loader.getResources(var1); } } catch (IOException var2) { ServiceLoader.fail(this.service, "Error locating configuration files", var2); } } while(this.pending == null || !this.pending.hasNext()) { if (!this.configs.hasMoreElements()) { return false; } this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement()); } this.nextName = (String)this.pending.next(); return true; } } public S next() { if (!this.hasNext()) { throw new NoSuchElementException(); } else { String var1 = this.nextName; this.nextName = null; Class var2 = null; try { // 通過反射方法Class.forName()載入類物件 var2 = Class.forName(var1, false, this.loader); } catch (ClassNotFoundException var5) { ServiceLoader.fail(this.service, "Provider " + var1 + " not found"); } if (!this.service.isAssignableFrom(var2)) { ServiceLoader.fail(this.service, "Provider " + var1 + " not a subtype"); } try { // 呼叫instance()方法將類例項化 Object var3 = this.service.cast(var2.newInstance()); // 儲存容器 ServiceLoader.this.providers.put(var1, var3); // 並返回例項 return var3; } catch (Throwable var4) { ServiceLoader.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4); throw new Error(); } } } // 禁止刪除 public void remove() { throw new UnsupportedOperationException(); } } 複製程式碼
5. 總結
優點:使用Java SPI機制的優勢是實現解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。
缺點:執行緒不安全,雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。如果你並不想用某些實現類,它也被載入並例項化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個引數來獲取對應的實現類。