「Java 路線」| 服務(wù)發(fā)現(xiàn)框架 ServiceLoader

點(diǎn)贊關(guān)注,不再迷路,你的支持對(duì)我意義重大!

?? Hi,我是丑丑。本文 「Java 路線」| 導(dǎo)讀 —— 他山之石,可以攻玉 已收錄,這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)


目錄


1. 前置知識(shí)

這篇文章的內(nèi)容會(huì)涉及以下前置 / 相關(guān)知識(shí),貼心的我都幫你準(zhǔn)備好了,請(qǐng)享用~


2. 什么是服務(wù)發(fā)現(xiàn)?

服務(wù)發(fā)現(xiàn)(Service Provider Interface,SPI)是一個(gè)服務(wù)的注冊(cè)與發(fā)現(xiàn)機(jī)制,通過 解耦服務(wù)提供者與服務(wù)使用者,實(shí)現(xiàn)了服務(wù)創(chuàng)建 & 服務(wù)使用的關(guān)注點(diǎn)分離。

服務(wù)發(fā)現(xiàn)可以理解為控制反轉(zhuǎn)的一種實(shí)現(xiàn)形式,即:當(dāng)「調(diào)用方對(duì)象」需要使用「依賴項(xiàng)」時(shí),不再直接「構(gòu)造對(duì)象」,取而代之的是由外部 IoC 容器來創(chuàng)建并注入調(diào)用方。 IoC 可以認(rèn)為是一種設(shè)計(jì)模式,但是由于理論成熟的時(shí)間相對(duì)較晚,所以沒有包含在《設(shè)計(jì)模式·GoF》之中。

控制反轉(zhuǎn)的實(shí)現(xiàn)方式主要有兩種:

  • 1、服務(wù)提供模式:從外部服務(wù)容器抓取依賴對(duì)象
  • 2、依賴注入:并以參數(shù)的形式注入依賴對(duì)象

兩者的本質(zhì)區(qū)別是:使用服務(wù)提供模式時(shí),調(diào)用方可以控制請(qǐng)求依賴對(duì)象的時(shí)機(jī);而使用依賴項(xiàng)注入方式時(shí),一般由外部主動(dòng)注入所需對(duì)象。

服務(wù)提供模式可以為我們帶來以下好處:

  • 1、在外部注入或配置依賴項(xiàng),因此我們可以重用這些組件。當(dāng)我們需要修改依賴項(xiàng)的實(shí)現(xiàn)時(shí),不需要大量修改很多處代碼,只需要修改一小部分代碼;
  • 2、可以注入依賴項(xiàng)的模擬實(shí)現(xiàn),讓代碼測(cè)試更加容易。

3. ServiceLoader 使用步驟

在分析 ServiceLoader 的使用原理之前,我們先來介紹下 ServiceLoader 的使用步驟。我們直接以 JDBC 作為例子,具體如下:

我們都知道 JDBC 編程有五大基本步驟:

  • 1、(非必須)執(zhí)行數(shù)據(jù)庫驅(qū)動(dòng)類加載:
Class.forName("com.mysql.jdbc.driver")
  • 2、連接數(shù)據(jù)庫:
DriverManager.getConnection(url, user, password) 
  • 3、創(chuàng)建SQL語句:
Connection#.creatstatement();
  • 4、執(zhí)行SQL語句并處理結(jié)果集:
Statement#executeQuery()
  • 5、釋放資源:
ResultSet#close()
Statement#close()
Connection#close()

其中「2、連接數(shù)據(jù)庫」內(nèi)部就是用了 ServiceLoader。為什么連接數(shù)據(jù)庫需要使用 SPI 設(shè)計(jì)思想呢?因?yàn)椴僮鲾?shù)據(jù)庫需要使用廠商提供的數(shù)據(jù)庫驅(qū)動(dòng)程序,如果直接使用廠商的驅(qū)動(dòng)耦合太強(qiáng)了,而使用 SPI 設(shè)計(jì)就能夠?qū)崿F(xiàn)服務(wù)提供者與服務(wù)使用者解耦。

下面,我們一步步手寫 JDBC 中關(guān)于 ServiceLoader 的相關(guān)源碼:

步驟1:定義服務(wù)接口

抽象出一個(gè)服務(wù)接口,這個(gè)接口將由數(shù)據(jù)庫驅(qū)動(dòng)實(shí)現(xiàn)類實(shí)現(xiàn):

public interface Driver {
    創(chuàng)建數(shù)據(jù)庫連接
    Connection connect(String url, java.util.Properties info);
    ...
}

步驟2:實(shí)現(xiàn)服務(wù)接口

數(shù)據(jù)庫廠商提供一個(gè)或多個(gè)實(shí)現(xiàn) Driver 接口的驅(qū)動(dòng)實(shí)現(xiàn)類,以 mysql 和 oracle 為例:

  • mysqlcom.mysql.cj.jdbc.Driver.java
已簡化
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        注冊(cè)驅(qū)動(dòng)
        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();
            注冊(cè)驅(qū)動(dòng)
            DriverManager.registerDriver(defaultDriver);
        }
    }
    ...
}

步驟3:注冊(cè)實(shí)現(xiàn)類到配置文件

在工程目錄 java 的同級(jí)目錄中新建目錄resources/META-INF/services,新建一個(gè)配置文件java.sql.Driver(文件名為服務(wù)接口的全限定名),文件中每一行是實(shí)現(xiàn)類的全限定名,例如:

com.mysql.cj.jdbc.Driver

我們可以解壓mysql-connector-java-8.0.19.jar包,找到對(duì)應(yīng)的 META-INF 文件夾。

步驟4:(使用方)加載服務(wù)

DriverManaer.java

已簡化
static {
    loadInitialDrivers();
}

private static void loadInitialDrivers() {
    ...
    1、讀取 "jdbc.drivers" 屬性
    String drivers = System.getProperty("jdbc.drivers");


    1、使用ServiceLoader遍歷實(shí)現(xiàn)類
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    2、獲得迭代器
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    3、迭代
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
    return null;
    ...
}

可以看到,DriverManager 被類加載時(shí)(static{})會(huì)調(diào)用 loadInitialDrivers()。這個(gè)方法內(nèi)部通過 ServiceLoader 提供的迭代器 Iterator<Driver> 遍歷了所有驅(qū)動(dòng)實(shí)現(xiàn)類。

那么,ServiceLoader 是如何實(shí)例化 Driver 接口的實(shí)現(xiàn)類的呢?下一節(jié),我們會(huì)深入 ServiceLoader 的源碼來解答這個(gè)問題。


4. ServiceLoader 源碼解析

4.1 入口方法

ServiceLoader 提供了三個(gè)靜態(tài)泛型工廠方法,內(nèi)部最終將調(diào)用ServiceLoader.load(Class,ClassLoader)

方法 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);
}

可以看到,三個(gè)方法僅在傳入的類加載器不同。

4.2 構(gòu)造方法

已簡化
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、實(shí)例化 LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}

可以看到,ServiceLoader 的構(gòu)造器中實(shí)例化了一個(gè) LazyIterator 迭代器的實(shí)例,這是一個(gè)「懶加載」的迭代器。這個(gè)迭代器里做了什么呢?

4.3 LazyIterator 迭代器

3、實(shí)例化 LazyIterator

private static final String PREFIX = "META-INF/services/";

private class LazyIterator implements Iterator<S> {

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    
    @Override
    public boolean hasNext() {
        return hasNextService();
    }

    @Override
    public S next() {
        return nextService();
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            // configs 未初始化才執(zhí)行
            3.1 配置文件:META-INF/services/服務(wù)接口的全限定名
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        }
        
        3.2 解析配置文件資源的迭代器
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        3.3 下一個(gè)實(shí)現(xiàn)類的全限定名
        nextName = pending.next();
        return true;
    }

    private S nextService() {
        if (!hasNextService()) throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;

        4.1 使用類加載器loader加載(不執(zhí)行初始化)
        Class<?> c = Class.forName(cn, false, loader);
        if (!service.isAssignableFrom(c)) { 檢查是否實(shí)現(xiàn) S 接口
            ClassCastException cce = new ClassCastException(service.getCanonicalName() + " is not assignable from " + c.getCanonicalName());
            fail(service, "Provider " + cn  + " not a subtype", cce);
        }

        4.2 使用反射創(chuàng)建服務(wù)類實(shí)例
        S p = service.cast(c.newInstance());

        4.3 服務(wù)實(shí)現(xiàn)類緩存到 providers
        providers.put(cn, p);
        return p;
    }
}

-> 3.2 解析配置文件資源的迭代器
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    3.2.1 使用 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();
}

以上代碼已經(jīng)非常簡化了,LazyIterator 的要點(diǎn)如下:

  • hasNext() 判斷邏輯:

    • 3.1 配置文件:「META-INF/services/服務(wù)接口的全限定名」;
    • 3.2 解析配置文件資源的迭代器;
    • 3.3 下一個(gè)實(shí)現(xiàn)類的全限定名。
  • next() 邏輯:

    • 4.1 使用類加載器 loader 加載(不執(zhí)行初始化);
    • 4.2 使用反射創(chuàng)建服務(wù)類實(shí)例;
    • 4.3 服務(wù)實(shí)現(xiàn)類緩存到 providers。

小結(jié)一下:

LazyInterator 會(huì)解析「META-INF/services/服務(wù)接口的全限定名」配置,遍歷每個(gè)服務(wù)實(shí)現(xiàn)類全限定類名,執(zhí)行類加載(未初始化),最后將服務(wù)實(shí)現(xiàn)類緩存到 providers。

這個(gè)迭代器在哪里使用的呢?繼續(xù)往下看~

4.4 包裝迭代器

其實(shí) ServiceLoader 本身就是實(shí)現(xiàn) Iterable 接口的:

public final class ServiceLoader<S> implements Iterable<S>

讓我們來看看 ServiceLoader 中的 Iterable#iterator() 是如何實(shí)現(xiàn)的:

private LazyIterator lookupIterator;

4、返回一個(gè)新的迭代器,包裝了providers和lookupIterator
public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

        @Override
        public boolean hasNext() {
            4.1 優(yōu)先從knownProviders取
            if (knownProviders.hasNext()) return true;
            return lookupIterator.hasNext();
        }

        @Override
        public S next() {
            4.2 優(yōu)先從knownProviders取
            if (knownProviders.hasNext()) return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

可以看到,ServiceLoader 里有一個(gè)泛型方法 Iterator<S> iterator(),它包裝了 providers 集合迭代器和 lookupIterator 兩個(gè)迭代器,迭代過程中優(yōu)先從 providers 獲取元素。providers 就是存放 LazyLoader 中存放類的地方。

private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

5. ServiceLoader 源碼小結(jié)

理解 ServiceLoader 源碼之后,我們總結(jié)要點(diǎn)如下:

5.1 約束

1、服務(wù)實(shí)現(xiàn)類必須實(shí)現(xiàn)服務(wù)接口 S(if (!service.isAssignableFrom(c)));
2、服務(wù)實(shí)現(xiàn)類需包含無參的構(gòu)造器,LazyInterator 是反射創(chuàng)建實(shí)現(xiàn)類市里的(S p = service.cast(c.newInstance()));
3、配置文件需要使用 UTF-8 編碼(new BufferedReader(new InputStreamReader(in, "utf-8")))。

5.2 懶加載

ServiceLoader 使用「懶加載」的方式創(chuàng)建服務(wù)實(shí)現(xiàn)類實(shí)例,只有在迭代器推進(jìn)的時(shí)候才會(huì)創(chuàng)建實(shí)例(nextService())。

5.3 內(nèi)存緩存

ServiceLoader 使用 LinkedHashMap 緩存創(chuàng)建的服務(wù)實(shí)現(xiàn)類實(shí)例。

提示: LinkedHashMap 在迭代時(shí)會(huì)按照 Map#put 執(zhí)行順序遍歷。

5.4 沒有服務(wù)注銷機(jī)制

服務(wù)實(shí)現(xiàn)類實(shí)例被創(chuàng)建后,它的垃圾回收的行為與 Java 中的其他對(duì)象一樣,只有這個(gè)對(duì)象沒有到 GC Root 的強(qiáng)引用,才能作為垃圾回收。而 ServiceLoader 內(nèi)部只有一個(gè)方法來清楚 provices 內(nèi)存緩存。

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

6. 服務(wù)實(shí)現(xiàn)的選擇

當(dāng)存在多個(gè)提供者時(shí),不一定使用全部的服務(wù)實(shí)現(xiàn)類,而是需要根據(jù)某些特性篩選一種最佳實(shí)現(xiàn)。ServiceLoader 機(jī)制只能在遍歷整個(gè)迭代器的過程中,從發(fā)現(xiàn)的實(shí)現(xiàn)類中決策出一個(gè)最佳實(shí)現(xiàn)。

舉個(gè)例子,我們可以使用字符集的表示符號(hào)來獲得一個(gè)對(duì)應(yīng)的 Charset 對(duì)象:Charset.forName(String),這個(gè)方法里面就只會(huì)選擇匹配的 Charaset 對(duì)象。

CharsetProvider.java

服務(wù)接口
public abstract class CharsetProvider {
    public abstract Charset charsetForName(String charsetName);
    // 省略其他方法...
}

Charset.java

public static Charset forName(String charsetName) {
    以下只摘要與ServiceLoader有關(guān)的邏輯...

    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;
    }
}

7. 總結(jié)

  • 服務(wù)發(fā)現(xiàn) SPI 是控制反轉(zhuǎn) IoC 的實(shí)現(xiàn)方式之一,而 ServiceLoader 是 JDK 中實(shí)現(xiàn)的 SPI 框架;

  • ServiceLoader 本身就是一個(gè) Iterable 接口,迭代時(shí)會(huì)從 META-INF/services 配置中解析接口實(shí)現(xiàn)類的全限定類名,使用反射創(chuàng)建服務(wù)實(shí)現(xiàn)類對(duì)象;

  • SPI 是一個(gè)相對(duì)簡易的框架,為了滿足復(fù)雜業(yè)務(wù)的需要,一般會(huì)使用其他第三方框架,例如后臺(tái)的 Dubbo、客戶端的 ARouter 與 WMRouter等。

在后面的文章中,我將與你探討 Android 組件化實(shí)踐,并附帶阿里 ARouter 源碼分析。點(diǎn)擊關(guān)注,趕緊上車~ 「Android 路線」| 導(dǎo)讀 —— 從零到無窮大


創(chuàng)作不易,你的「三連」是丑丑最大的動(dòng)力,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。