JAVA SPI解析
在閱讀Dubbo源碼時發(fā)現(xiàn)Dubbo針對java的spi機制做了擴展。那么spi究竟是什么呢?
SPI 是什么
SPI全稱為Service Provider Interface,是Java提供的用來加載第三方實現(xiàn)的service的API。
比如java.sql.Driver接口,第三方廠商比如MySql,PostgreSql均會實現(xiàn)這個接口來提供接入數(shù)據(jù)庫的實現(xiàn),Java的SPI機制就可以某個接口尋找服務實現(xiàn)。
當服務提供者實現(xiàn)了一種接口后,需要在classpath的META-INF/services中創(chuàng)建一個以服務接口名命名的文件,文件的內(nèi)容是這個接口的具體實現(xiàn)類。當調(diào)用者需要用到這個服務時,就會查找對應jar包下的META-INF/services中的配置,如果找到了,那么就會實例化這個實現(xiàn)類,調(diào)用者就可以調(diào)用對應的服務了。Java中,查找實現(xiàn)類的工具類是java.util.ServiceLoader。
一個DEMO
接下來我們自己實現(xiàn)一個簡單的SPI服務,從中可以學習到SPI的機制。

- api為接口,其下有一個名為
People的interface,提供一個hello的方法
image - jack與jenny為
People的實現(xiàn)module,各自繼承People接口,并實現(xiàn)自己的代碼邏輯,本例為了簡單只是單純的輸出了各自的名字,如下所示:
image
image - jack與jenny分別在自己module下的resources文件夾下創(chuàng)建
META-INF/services文件夾,并且在該文件夾下創(chuàng)建一個名為io.wkz.People的文件,文件內(nèi)容分別為Jack與Jenny的全限定名。其結構如下:
image - service依賴上面三個module,并且在main函數(shù)中,使用
ServiceLodaer加載People的實現(xiàn)類,并且分別調(diào)用hello方法,如下所示:
image
其輸出如下:
image
觀察main函數(shù)會發(fā)現(xiàn),我們并沒有在main函數(shù)下實例化Jack或Jenny,但是我們在調(diào)用服務時,仍然調(diào)用到了這兩個類的實例。從這里可以發(fā)現(xiàn),SPI的核心思想其實就是解耦。
原理解析
既然在上一個例子里我們是使用的ServiceLoader來獲取不同的服務提供者,那么我們就進入ServiceLoader源碼探索一番吧。
首先查看一下ServiceLoader的屬性字段
public final class ServiceLoader<S>
implements Iterable<S>
{
//看屬性名可知此字段就是查找配置文件的前綴文件夾名,從這里也了解了為什么必須要在META-INF/services下新增配置文件。
private static final String PREFIX = "META-INF/services/";
// 代表被加載的類或者接口
private final Class<S> service;
// 類加載器,用于加載并實例化服務
private final ClassLoader loader;
// 創(chuàng)建ServiceLoader時采用的訪問控制上下文
private final AccessControlContext acc;
// 實例化后的服務緩存,用的是LinkedHashMap,所以是按順序排列的,具體排列順序是按實例化的先后順序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器,Lazy代表是懶的
private LazyIterator lookupIterator;
...
}
屬性字段上比較簡單,接下來我們看一下ServiceLoader.load方法,跟蹤代碼發(fā)現(xiàn)其最終是實例化了一個ServiceLoader類并返回。繼續(xù)進入構造方法
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();
}
構造函數(shù)里前面三行屬于防御式編程,將null處理掉,要么報錯,要么設置默認值。最后一行的reload方法是清空providers,并且實例化一個LazyIterator用于后期迭代。
LazyIterator也比較簡單,看名字就是實現(xiàn)了Iterator接口的。比較關鍵的方法是nextService
private S nextService() {
//防御式編程
if (!hasNextService())
throw new NoSuchElementException();
//nextName是在hasNextService方法中賦值的,這里獲取到就置空
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用當前的ClassLoader加載類
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
//加載失敗,拋錯。
fail(service,
"Provider " + cn + " not found");
}
//判斷加載的類是不是當前service的子類或者實現(xiàn)類。
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//實例化類。注意這里,這步表明我們實現(xiàn)的service必須要有一個無參構造函數(shù)
S p = service.cast(c.newInstance());
//進行緩存。這里可以看ServiceLoader的Iterator實現(xiàn)。是先判斷的providers是不是hasNext,如果不是,則進入到本LazyIterator中,從一定程度上解決了創(chuàng)建時就加載全部實現(xiàn)類的問題,但是并沒有治本。
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
//防御式編程
throw new Error(); // This cannot happen
}
從代碼層面上看,我們可以得出幾點結論:
- ServiceLoader是懶加載,有緩存的。
- ServiceLoader加載時,全部加載,沒有提供單個service獨立加載的方法,如果調(diào)用方只希望調(diào)用部分實現(xiàn)類,目前ServiceLoader并不支持。
- 雖然ServiceLoader內(nèi)部使用了LinkedHashMap,但是并沒有對外暴露接口,只能通過Iterator訪問。
- 并發(fā)訪問同一個ServiceLoader是線程不安全的。
總結
Java提供的SPI機制,優(yōu)勢在于解耦,調(diào)用方不需要明確耦合第三方實現(xiàn)類,而是交由ServiceLoader提供。當程序需要使用另一種第三方實現(xiàn)類時,直接替換依賴就可以,無需重構代碼。
缺點其實在上面說到了。這里就不贅述了。Dubbo其實也是針對這些默認SPI的缺點才自己實現(xiàn)了Dubbo SPI。





