1. spi 是什麼
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實作或者擴充的API,它可以用來啟用架構擴充和替換元件。
系統設計的各個抽象,往往有很多不同的實作方案,在面向的對象的設計裡,一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。一旦代碼裡涉及具體的實作類,就違反了開閉原則,Java SPI就是為某個接口尋找服務實作的機制,Java Spi的核心思想就是解耦。
整體機制圖如下:
Java SPI 實際上是“基于接口的程式設計+政策模式+配置檔案”組合實作的動态加載機制。
總結起來就是:調用者根據實際使用需要,啟用、擴充、或者替換架構的實作政策
2. 應用場景
-
資料庫驅動加載接口實作類的加載
JDBC加載不同類型資料庫的驅動
-
日志門面接口實作類加載
SLF4J加載不同提供應商的日志實作類
-
Spring
Servlet容器啟動初始化
org.springframework.web.SpringServletContainerInitializer
-
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
⚠️SPI的實作類必須攜帶一個不帶參數的構造方法;PayService
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.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;
// 建立ServiceLoader時擷取的通路控制上下文
private final AccessControlContext acc;
// 按執行個體化順序緩存Provider
private LinkedHashMap<String, S> providers = new LinkedHashMap();
// 懶加載疊代器
private LazyIterator lookupIterator;
......
}
參考具體ServiceLoader具體源碼,代碼量不多,實作的流程如下:
- 應用程式調用ServiceLoader.load方法
// 1. 擷取ClassLoad public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } // 2. 調用構造方法 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){ return new ServiceLoader<>(service, loader); } // 3. 校驗參數和ClassLoad private ServiceLoader(Class<S> svc, ClassLoader cl) { service = Objects.requireNonNull(svc, "Service interface cannot be null"); loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } //4. 清理緩存容器,執行個體懶加載疊代器 public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
- 我們簡單看一下這個懶加載疊代器
// 實作完全懶惰的提供程式查找的私有内部類 private class LazyIterator implements Iterator<S>{ // 需要加載的服務的類或接口 Class<S> service; // 用于定位、加載和執行個體化提供程式的類加載器 ClassLoader loader; // 枚舉類型的資源路徑 Enumeration<URL> configs = null; // 疊代器 Iterator<String> pending = null; // 配置檔案中下一行className String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } // 加載配置PREFIX + service.getName()的檔案 if (configs == null) { try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } // 循環擷取下一行 while ((pending == null) || !pending.hasNext()) { // 判斷是否還有元素 if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } // 擷取類名 nextName = pending.next(); return true; } // 擷取下一個Service實作 private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 加載類 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } // 超類判斷 if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { // 執行個體化并進行類轉換 S p = service.cast(c.newInstance()); // 放入緩存容器中 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } // for循環周遊時 public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction<S> action = new PrivilegedAction<S>() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } // 禁止删除 public void remove() { throw new UnsupportedOperationException(); } }
- 将給定URL的内容作為提供程式配置檔案進行分析。
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError { InputStream in = null; BufferedReader r = null; ArrayList<String> names = new ArrayList<>(); try { in = u.openStream(); r = new BufferedReader(new InputStreamReader(in, "utf-8")); int lc = 1; while ((lc = parseLine(service, u, r, lc, names)) >= 0); } catch (IOException x) { fail(service, "Error reading configuration file", x); } finally { try { if (r != null) r.close(); if (in != null) in.close(); } catch (IOException y) { fail(service, "Error closing configuration file", y); } } return names.iterator(); }
- 按行解析配置檔案,并儲存names清單中
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError { String ln = r.readLine(); if (ln == null) { return -1; } int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) fail(service, u, lc, "Illegal configuration-file syntax"); int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) fail(service, u, lc, "Illegal provider-class name: " + ln); for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) fail(service, u, lc, "Illegal provider-class name: " + ln); } // 判斷provider容器中是否包含 不包含則講classname加入 names清單中 if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln); } return lc + 1; }
5. 總結
優點:使用Java SPI機制的優勢是實作解耦,使得第三方服務子產品的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用架構擴充或替換架構元件。
缺點:線程不安全,雖然ServiceLoader也算是使用的延遲加載,但是基本隻能通過周遊全部擷取,也就是接口的實作類全部加載并執行個體化一遍。如果你并不想用某些實作類,它也被加載并執行個體化了,這就造成了浪費。擷取某個實作類的方式不夠靈活,隻能通過Iterator形式擷取,不能根據某個參數來擷取對應的實作類。