Java SPI 原始碼解析
注:程式碼環境基於 JDK 1.8
一、SPI 是什麼?
SPI(Service Provider Interface):是一個可以被第三方擴充套件或實現的 API,它可以用來實現框架擴充套件和可替換的模組,優勢是實現解耦。簡單來說就是推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。若在程式碼裡涉及具體的實現類就違反了可挺拔的原則。從而 java SPI 提供了這種服務發現機制:為某個介面尋找服務實現的機制。
二、SPI 與 API 的區別
- API 直接為提供了功能,使用 API 就能完成任務。
- API 和 SPI 都是相對的概念,差別只在語義上,API 直接被應用開發人員使用,SPI 被框架擴張人員使用。
- API 大多數情況下,都是實現方來制定介面並完成對介面的不同實現,呼叫方僅僅依賴卻無權選擇不同實現。SPI 是呼叫方來制定介面,實現方來針對介面來實現不同的實現。呼叫方來選擇自己需要的實現方。
三、SPI 使用及示例
- 服務呼叫方通過 ServiceLoader.load 載入服務介面的實現類例項
- 服務提供方實現服務介面後, 在自己Jar包的 META-INF/services 目錄下新建一個介面名全名的檔案, 並將具體實現類全名寫入。
示例:
1. 建立介面:
public interface Search { List<String> searchDoc(String keyword); }
2. 建立 DatabaseSearch 實現類:
public class DatabaseSearch implements Search {
@Override
public List<String> searchDoc(String keyword) {
System.out.printf("資料庫搜尋:" + keyword);
return null;
}
}
3. 建立 FileSearch 實現類:
public class FileSearch implements Search { @Override public List<String> searchDoc(String keyword) { System.out.println("檔案搜尋:" + keyword); return null; } }
4. META-INF.services 中建立介面全限定名檔案:spi.learn.Search:
spi.learn.FileSearch
spi.learn.DatabaseSearch
5. 測試類:
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("spi");
}
}
-----輸出:-----
檔案搜尋:spi
資料庫搜尋:spi
四、原始碼解讀
先來看下 ServiceLoader 類的全域性變數:
//spi 預設載入的路徑
private static final String PREFIX = "META-INF/services/";
// 表示正在被載入的類或介面
// The class or interface representing the service being loaded
private final Class<S> service;
// 用於定位、裝入和例項化提供程式的類載入器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 許可權控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 基於例項的順序快取類的實現例項,其中Key為實現類的全限定類名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 當前的"懶查詢"迭代器,ServiceLoader的核心
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
ServiceLoader.load(Search.class) 載入入口:
public static <S> ServiceLoader<S> load(Class<S> service) {
//獲取當前執行緒的類載入器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
// 因為 ServiceLoader 的構造為私有,這裡只能依賴此靜態方法來訪問私有構造例項化,典型的靜態工廠方法。
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// svc 為 null,拋 NullPointerException
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 若沒有指定載入器,預設使用系統載入器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// Java安全管理器
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
// 清空例項化好的快取。
providers.clear();
// "懶查詢",ServiceLoader 的核心。
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator 為 ServiceLoader 的核心,來看看具體原始碼:
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
// 載入資源的URL集合
Enumeration<URL> configs = null;
// 需載入的實現類的全限定類名的集合
Iterator<String> pending = null;
// 下一個需要載入的實現類的全限定類名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
//資源已存在,無需載入
if (nextName != null) {
return true;
}
// 資源為null,嘗試載入
if (configs == null) {
try {
// 資源名稱,META-INF/services + 全限定名
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;
}
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 反射構造 Class 例項
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());
// 例項完成,新增快取,Key:實現類全限定類名,Value:實現類例項
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}
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();
}
}
LazyIterator 機制總結:LazyIterator 也實現了 Iterator介面的實現,Lazy特性體現在只有在使用 ServiceLoader 呼叫 iterator() 方法獲取 Iterator 介面匿名實現類後, 再呼叫 hasNext() 方法時,才會"懶判斷"或者"懶載入"下一個實現類的例項。呼叫的入口,也就是示例 main 方法中 while 那一步,我們再看下 iterator() 的 Iterator 匿名實現類原始碼:
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext()) return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext()) return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
呼叫鏈分析完,最後看下 hasNextService 中解析限定名檔案的 parse 方法,主要檢查檔案內容的字符合法性、快取過濾避免重複載入。
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
// 存放 META-INF/services 下檔案中的實現類的全類名
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();
}
//具體解析資原始檔中每一行內容
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; //-1表示解析完成
}
// 如果存在'#'字元,擷取第一個'#'字串之前的內容,'#'字元之後的屬於註釋內容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
//不合法的標識:' '、'\t'
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
//判斷第一個 char 是否一個合法的 Java 起始識別符號
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
//判斷所有其他字串是否屬於合法的Java識別符號
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);
}
//不存在則快取
if (!providers.containsKey(ln) && !names.contains(ln)) names.add(ln);
}
return lc + 1;
}
五、總結
優點:
- 使用 Java SPI 機制的優勢是實現解耦,使第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。
- 相比使用提供介面 jar 包供第三方服務使用的方式,SPI 使得源框架不必關心介面的實現類的路徑,可以不使用硬編碼 import 匯入實現類。
缺點:
- 雖然 ServiceLoader 使用了懶載入,但結果還是通過遍歷獲取,基本上可以說是全部例項化了一遍,所以說,這個懶載入機制在此場景下是浪費的。
- 由於是遍歷獲取,所以獲取實現類的方式不夠靈活。
- 多個併發多執行緒使用 ServiceLoader 類的例項是不安全的。
注:文章中難免有不足之處,歡迎評論、互動、指正。