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 為例:

  • mysqlcom.mysql.cj.jdbc.Driver.java
// 已簡化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        // 註冊驅動
        java.sql.DriverManager.registerDriver(new Driver());
    }
    ...
}
  • oracleoracle.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 遍歷了所有驅動實現類。那麼,ServiceLoader 是如何實例化 Driver 介面的實現類的呢?下一節,我們會深入 ServiceLoader 的源碼來解答這個問題。


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 知識體系。技術和職場問題,請關注公眾號 [彭旭銳] 私信我提問。

Tags: