SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它能夠用來啓用框架擴展和替換組件。java
系統設計的各個抽象,每每有不少不一樣的實現方案,在面向的對象的設計裏,通常推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了開閉原則,Java SPI就是爲某個接口尋找服務實現的機制,Java Spi的核心思想就是解耦。web
總體機制圖以下:spring
Java SPI 其實是「基於接口的編程+策略模式+配置文件」組合實現的動態加載機制。數據庫
總結起來就是:調用者根據實際使用須要,啓用、擴展、或者替換框架的實現策略編程
數據庫驅動加載接口實現類的加載緩存
JDBC加載不一樣類型數據庫的驅動安全
日誌門面接口實現類加載微信
SLF4J加載不一樣提供應商的日誌實現類架構
Springapp
Servlet容器啓動初始化org.springframework.web.SpringServletContainerInitializer
Spring Boot
自動裝配過程當中,加載META-INF/spring.factories文件,解析properties文件
Dubbo
Dubbo大量使用了SPI技術,裏面有不少個組件,每一個組件在框架中都是以接口的造成抽象出來
例如Protocol 協議接口
以支付服務爲例:
建立一個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.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));
}
}
}
複製代碼
運行測試類,查看返回結果
使用支付寶支付
複製代碼
首先,咱們先打開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;
}
複製代碼
優勢:使用Java SPI機制的優點是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一塊兒。應用程序能夠根據實際業務狀況啓用框架擴展或替換框架組件。
缺點:線程不安全,雖然ServiceLoader也算是使用的延遲加載,可是基本只能經過遍歷所有獲取,也就是接口的實現類所有加載並實例化一遍。若是你並不想用某些實現類,它也被加載並實例化了,這就形成了浪費。獲取某個實現類的方式不夠靈活,只能經過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
歡迎你們加入Java高級架構羣 378461078