JDK 自帶的服務發現框架 ServiceLoader 好用嗎?
請點贊關注,你的支援對我意義重大。
🔥 Hi,我是小彭。本文已收錄到 Github · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。
前言
大家好,我是小彭。
過去兩年,我們在掘金平台上發布過一些文章,小彭也受到了大家的意見和鼓勵。最近,小彭會陸續搬運到公眾號上。
學習路線圖:
1. 認識服務發現?
1.1 什麼是服務發現
服務發現(Service Provider Interface,SPI)是一個服務的註冊與發現機制,通過解耦服務提供者與服務使用者,實現了服務創建 & 服務使用的關注點分離。 服務提供模式可以為我們帶來以下好處:
- 1、在外部注入或配置依賴項,因此我們可以重用這些組件。當我們需要修改依賴項的實現時,不需要大量修改很多處程式碼,只需要修改一小部分程式碼;
- 2、可以注入依賴項的模擬實現,讓程式碼測試更加容易。
服務發現示意圖
1.2 服務發現和依賴注入的區別
服務發現和依賴注入都是控制反轉 Ioc 的實現形式之一。 IoC 可以認為是一種設計模式,但是由於理論成熟的時間相對較晚,所以沒有包含在《設計模式 · GoF》之中,即: 當依賴方需要使用依賴項時,不再直接構造對象,而是由外部 IoC 容器來創建並提供依賴。
- 1、服務提供模式: 從外部服務容器抓取依賴對象,調用方可以 「主動」 控制請求依賴對象的時機;
- 2、依賴注入: 並以參數的形式注入依賴對象,調用方 「被動」 接收外部注入的依賴對象。
2. JDK ServiceLoader 的使用步驟
在分析 ServiceLoader 的使用原理之前,我們先來介紹下 ServiceLoader 的使用步驟。
我們直接以 JDBC 作為例子,其中「2、連接資料庫」內部就是用了 ServiceLoader。為什麼連接資料庫需要使用 SPI 設計思想呢?因為操作資料庫需要使用廠商提供的資料庫驅動程式,如果直接使用廠商的驅動耦合太強了,而使用 SPI 設計就能夠實現服務提供者與服務使用者解耦。
以下為使用步驟,具體分為 5 個步驟:
- 1、(非必須)執行資料庫驅動類載入:
Class.forName("com.mysql.jdbc.driver")
- 2、連接資料庫:
DriverManager.getConnection(url, user, password)
- 3、創建SQL語句:
Connection#.creatstatement();
- 4、執行SQL語句並處理結果集:
Statement#executeQuery()
- 5、釋放資源:
ResultSet#close()
Statement#close()
Connection#close()
下面,我們一步步手寫 JDBC 中關於 ServiceLoader 的相關源碼:
步驟 1:定義服務介面
定義一個驅動介面,這個介面將由資料庫驅動實現類實現。在服務發現框架中,這個介面就是服務介面。
public interface Driver {
// 創建資料庫連接
Connection connect(String url, java.util.Properties info);
...
}
步驟 2:實現服務介面
資料庫廠商提供一個或多個實現 Driver 介面的驅動實現類,以 mysql 和 oracle 為例:
- mysql:
com.mysql.cj.jdbc.Driver.java
// 已簡化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
// 註冊驅動
java.sql.DriverManager.registerDriver(new Driver());
}
...
}
- oracle:
oracle.jdbc.driver.OracleDriver.java
// 已簡化
public class OracleDriver implements Driver {
private static OracleDriver defaultDriver = null;
static {
if (defaultDriver == null) {
// 1、單例
defaultDriver = new OracleDriver();
// 註冊驅動
DriverManager.registerDriver(defaultDriver);
}
}
...
}
步驟3:註冊實現類到配置文件
在工程目錄 java
的同級目錄中新建目錄 resources/META-INF/services
,新建一個配置文件 java.sql.Driver
(文件名為服務介面的全限定名),文件中每一行是實現類的全限定名,例如:
com.mysql.cj.jdbc.Driver
我們可以解壓 mysql-connector-java-8.0.19.jar
包,找到對應的 META-INF 文件夾。
步驟4:(使用方)載入服務
DriverManaer.java
// 已簡化
static {
loadInitialDrivers();
}
// 入口
private static void loadInitialDrivers() {
...
// 讀取 "jdbc.drivers" 屬性
String drivers = System.getProperty("jdbc.drivers");
// 1、使用 ServiceLoader 遍歷 Driver 服務介面的實現類
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 2、獲得迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 3、迭代(ServiceLoader 內部會通過反射)
while(driversIterator.hasNext()) {
driversIterator.next();
}
return null;
...
}
可以看到,DriverManager 被類載入時(static{})會調用 loadInitialDrivers()
。這個方法內部通過 ServiceLoader 提供的迭代器 Iterator
3. ServiceLoader 源碼解析
3.1 ServiceLoader 入口方法
ServiceLoader 提供了三個靜態泛型工廠方法,內部最終將調用 ServiceLoader.load(Class, ClassLoader)
,其中第一個參數就是服務介面的 Class 對象。
ServiceLoader.java
// 方法 1:
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
// 使用 SystemClassLoader 類載入器
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
// 方法 2:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 使用執行緒上下文類載入器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 方法 3(最終走到這個方法):
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
可以看到,三個方法僅在傳入的類載入器不同,最終只是返回了一個面向服務介面 S 的 ServiceLoader 對象。我們先看一下構造器里做了什麼工作。
3.2 ServiceLoader 構造方法
ServiceLoader.java
// 已簡化
private final Class<S> service;
// 服務實現快取
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 1、類載入器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 2、清空 providers
providers.clear();
// 3、實例化 LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
可以看到,ServiceLoader 的構造器中主要就是實例化了一個 LazyIterator
迭代器的實例,這是一個「懶載入」的迭代器。這個迭代器里做了什麼呢?我們繼續往下看
3.3 LazyIterator 迭代器
ServiceLoader.java
// -> 3、實例化 LazyIterator
// 前文提到的配置文件路徑
private static final String PREFIX = "META-INF/services/";
private class LazyIterator implements Iterator<S> {
// 服務介面 Class 對象
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
// pending、nextName:用於解析配置文件中的服務實現類名
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
// 3.1 判斷是否有下一個服務實現
@Override
public boolean hasNext() {
return hasNextService();
}
// 3.2 返回下一個服務實現
@Override
public S next() {
return nextService();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
// -> 3.1 判斷是否有下一個服務實現
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
// 3.1.1 拼接配置文件路徑:META-INF/services/服務介面的全限定名
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
// 3.1.2 parse:解析配置文件資源的迭代器
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 3.1.3 下一個實現類的全限定名
nextName = pending.next();
return true;
}
// 3.2 返回下一個服務實現
private S nextService() {
if (!hasNextService()) throw new NoSuchElementException();
String cn = nextName;
nextName = null;
// 3.2.1 使用類載入器 loader 載入
Class<?> c = Class.forName(cn, false /* 不執行初始化 */, loader);
if (!service.isAssignableFrom(c)) {
// 檢查是否實現 S 介面
ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
fail(service, "Provider " + cn + " not a subtype", cce);
}
// 3.2.2 使用反射創建服務類實例
S p = service.cast(c.newInstance());
// 3.2.3 服務實現類快取到 providers
providers.put(cn, p);
return p;
}
}
// -> 3.1.2 parse:解析配置文件資源的迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
// 使用 UTF-8 編碼輸入配置文件資源
InputStream in = u.openStream();
BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"));
ArrayList<String> names = new ArrayList<>();
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
return names.iterator();
}
以上程式碼已經非常簡化了,LazyIterator 的要點如下:
- hasNext() 判斷邏輯:
- 3.1.1 拼接配置文件路徑:「META-INF/services/服務介面的全限定名」;
- 3.1.2 解析配置文件資源的迭代器;
- 3.1.3 找到下一個實現類的全限定名。
- next() 邏輯:
- 3.2.1 使用類載入器 loader 載入(不執行初始化);
- 3.2.2 使用反射創建服務類實例;
- 3.2.3 服務實現類快取到 providers。
小結一下: LazyInterator 會解析「META-INF/services/服務介面的全限定名」配置,遍歷每個服務實現類全限定類名,執行類載入(未初始化),最後將服務實現類快取到 providers。
那麼,這個迭代器在哪裡使用的呢?繼續往下看~
3.4 包裝迭代器
其實 ServiceLoader 本身就是實現 Iterable 介面的:
ServiceLoader.java
public final class ServiceLoader<S> implements Iterable<S>
讓我們來看看 ServiceLoader 中的 Iterable#iterator()
是如何實現的:
private LazyIterator lookupIterator;
// 4、返回一個新的迭代器,包裝了 providers 和 lookupIterator
public Iterator<S> iterator() {
return new Iterator<S>() {
// providers 就是上一節 next() 中快取的服務實現
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
@Override
public boolean hasNext() {
// 4.1 優先從 knownProviders 取,再從 LazyIterator 取
if (knownProviders.hasNext()) return true;
return lookupIterator.hasNext();
}
@Override
public S next() {
// 4.2 優先從 knownProviders 取,再從 LazyIterator 取
if (knownProviders.hasNext()) return knownProviders.next().getValue();
return lookupIterator.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
可以看到,ServiceLoader 里有一個泛型方法 Iterator<S> iterator(),它包裝了 providers 集合迭代器和 lookupIterator 兩個迭代器。對於已經 「發現」 的服務實現類會被快取到 providers 集合中,包裝類的作用就是優先讀取快取而已。
4. ServiceLoader 源碼分析總結
理解 ServiceLoader 源碼之後,我們總結要點如下:
4.1 約束
1、服務實現類必須實現服務介面 S( if (!service.isAssignableFrom(c))
);
2、服務實現類需包含無參的構造器,LazyInterator 是反射創建實現類市裡的( S p = service.cast(c.newInstance())
);
3、配置文件需要使用 UTF-8 編碼( new BufferedReader(new InputStreamReader(in, "utf-8"))
)。
4.2 懶載入
ServiceLoader 使用「懶載入」的方式創建服務實現類實例,只有在迭代器推進的時候才會創建實例( nextService()
)。
4.3 記憶體快取
ServiceLoader 使用 LinkedHashMap 快取創建的服務實現類實例。
提示: LinkedHashMap 在迭代時會按照 Map#put 執行順序遍歷。
4.4 沒有服務註銷機制
服務實現類實例被創建後,它的垃圾回收的行為與 Java 中的其他對象一樣,只有這個對象沒有到 GC Root 的強引用,才能作為垃圾回收。而 ServiceLoader 內部只有一個方法來完全清除 provices 記憶體快取。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
4.5 沒有服務篩選機制
當存在多個提供者時,ServiceLoader 沒有提供篩選機制,使用方只能在遍歷整個迭代器中的所有實現,從發現的實現類中決策出一個最佳實現。舉個例子,我們可以使用字符集的表示符號來獲得一個對應的 Charset 對象:Charset.forName(String)
,這個方法裡面就只會選擇匹配的 Charaset 對象。
CharsetProvider.java
服務介面
public abstract class CharsetProvider {
public abstract Charset charsetForName(String charsetName);
// 省略其他方法...
}
Charset.java
public static Charset forName(String charsetName) {
// 以下只摘要與 ServiceLoader 有關的邏輯...
ServiceLoader<CharsetProvider> sl = ServiceLoader.load(CharsetProvider.class, cl);
Iterator<CharsetProvider> i = sl.iterator();
for (Iterator<CharsetProvider> i = providers(); i.hasNext();) {
CharsetProvider cp = i.next();
// 滿足匹配條件,return
Charset cs = cp.charsetForName(charsetName);
if (cs != null)
return cs;
}
}
5. 總結
- 服務發現 SPI 是控制反轉 IoC 的實現方式之一,而 ServiceLoader 是 JDK 中實現的 SPI 框架。ServiceLoader 本身就是一個 Iterable 介面,迭代時會從
META-INF/services
配置中解析介面實現類的全限定類名,使用反射創建服務實現類對象; - ServiceLoader 是 JDK 自帶的服務發現框架,原理也相對簡單,比如 Charset、AnnocationProcessor 等功能都是基於 ServiceLoader 實現的。另一方面,ServiceLoader 是一個相對簡易的框架,為了滿足複雜業務的需要,一般會使用其他第三方框架,例如後台的 Dubbo、客戶端的 ARouter 與 WMRouter等。
我是小彭,帶你構建 Android 知識體系。技術和職場問題,請關注公眾號 [彭旭銳] 私信我提問。