Java SPI機制總結系列之萬字最詳細圖解Java SPI機制原始碼分析

朱季謙發表於2023-11-12

image

原創/朱季謙

我在《Java SPI機制總結系列之開發入門例項》一文當中,分享了Java SPI的玩法,但是這只是基於表面的應用。若要明白其中的原理實現,還需深入到底層原始碼,分析一番。

這裡再重溫一下SPI機制的概念:SPI,是Service Provider Interface的縮寫,即服務提供者介面,單從字面上看,可以這樣理解,該機制提供了一種可根據介面型別去動態載入出介面實現類物件的功能。打一個比喻,該機制就類似Spring容器,透過IOC將物件的建立交給Spring容器處理,若需要獲取某個類的物件,就從Spring容器裡取出使用即可。同理,在SPI機制當中,提供了一個類似Spring容器的角色,叫【服務提供者】,在程式碼執行過程中,若要使用到實現了某個介面的服務實現類物件,只需要將對應的介面型別交給服務提供者,服務提供者將會動態載入出所有實現了該介面的服務實現類物件,最後給到服務使用者使用。

image

接著前文的分享,可從以下三個步驟目錄去深入分析Java SPI機制原始碼實現——

  1. 建立服務提供者ServiceLoader物件,其內部生成一個可延遲載入介面對應實現類物件的迭代器LazyIterator,主要作用是讀取並解析META-INF/services/目錄下的配置檔案中service類名字,進而透過反射載入生成service類物件。
  2. 呼叫serviceLoader.iterator()返回一個內部實際是呼叫LazyIterator迭代器的匿名迭代器物件。
  3. 遍歷迭代器,逐行解析介面全類名所對應配置檔案中的service實現類的名字,透過反射生成物件快取到連結串列,最後返回。
//step 1 建立ServiceLoader物件,其內部生成一個可延遲載入介面對應實現類物件的迭代器LazyIterator,主要作用是讀取並解析META-INF/services/目錄下的配置檔案中service類名字,進而透過反射載入生成service類物件。
ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);
//step 2 呼叫serviceLoader.iterator()返回一個內部實際是呼叫LazyIterator迭代器的匿名迭代器物件。
Iterator<UserService> serviceIterator = serviceLoader.iterator();
//step 3 遍歷迭代器,逐行解析介面全類名所對應配置檔案中的service實現類的名字,透過反射生成物件快取到連結串列,最後返回。
    UserService service = serviceIterator.next();
    service.getName();
    }
}

整個過程這裡先做一個全面概括——ServiceLoader類會延遲載入UserService介面全名對應的META-INF/services/目錄下的配置檔案com.zhu.service.UserService。當找到對應介面全名檔案後,會逐行讀取檔案裡Class類名的字串,假如儲存的是“com.zhu.service.impl.AUserServiceImpl”和“com.zhu.service.impl.BUserServiceImpl”這兩個類名,那麼就會逐行取出,再透過反射【“Class類名”.newInstance()】,就可以建立出UserService介面對應的服務提供者物件。這些物件會以結構為<實現類名, 實現類物件>的Map形式,儲存到LinkedHashMap連結串列裡。該連結串列將由迭代器迴圈遍歷,取出每一個實現類物件。

畫一個流程圖說明,大概如下——
image

接下來,基於該全貌流程圖,分別對原始碼作分析。

一、建立服務提供者ServiceLoader物件,其內部生成一個可延遲載入介面對應實現類物件的迭代器LazyIterator,主要作用是讀取並解析META-INF/services/目錄下的配置檔案中service類名字,進而透過反射載入生成service類物件。

先看第一部分程式碼——

ServiceLoader<UserService> serviceLoader = ServiceLoader.load(UserService.class);

進入到ServiceLoader.load(UserService.class)方法裡,裡面基於當前執行緒通Thread.currentThread().getContextClassLoader()建立一個當前上下文的類載入器ClassLoader,該載入器在這裡主要是用來載入META-INF.services目錄下的檔案。

在load方法裡,將UserService.class和類載入器ClassLoader當作引數,交給ServiceLoader中的另一個過載方法ServiceLoader.load(service, cl)去做進一步具體實現。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

進入到ServiceLoader.load(service, cl),該方法裡建立了一個ServiceLoader物件,該物件預設執行了引數值分別為UserService.class和ClassLoader的帶參構造方法。

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader)
{
    return new ServiceLoader<>(service, loader);
}

根據字面意義,可以看出,ServiceLoader是一個專門負責載入服務的物件,在SPI機制裡,它充當專門提供介面實現服務物件的角色。

這裡就有兩個問題,它怎麼提供服務物件,它提供的是哪個介面的服務?

針對這兩個問題,基於傳進來的引數值UserService.class和類載入器ClassLoader,就已經能猜出答案裡,它將透過類載入器ClassLoader去載入實現UserService介面的具體服務類物件。

進入到ServiceLoader的帶參建構函式——

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

這裡暫時只需要關注loader和 reload(),而acc是專門用在服務實現類的安全許可權訪問方面的,本文暫未涉及到acc,後續會考慮專門寫一篇文分享下SPI下,如何實現服務實現類的安全許可權訪問。

傳進來的loader如果為空,那麼就使用ClassLoader.getSystemClassLoader(),即系統類載入器,可以簡單理解,無論如何,都會得到一個非空的類載入器。

接著進入到reload()方法裡——

/**
 * Clear this loader's provider cache so that all providers will be reloaded.
 * 清除此載入器的提供程式快取,以便重新載入所有提供程式。
 * <p> After invoking this method, subsequent invocations of the {@link
 * #iterator() iterator} method will lazily look up and instantiate providers from scratch, 
   just as is done by a newly-created loader.
   呼叫此方法後,後續呼叫{@link #iterator() iterator}方法將從零開始惰性查詢並例項化提供商,
   就像新建立的載入器一樣。
 *
 * <p> This method is intended for use in situations in which new providers
 * can be installed into a running Java virtual machine.
   此方法旨在用於新提供者可以安裝到正在執行的Java虛擬機器中。
 */
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

根據reload() 方法的註釋說明,可以看到,該方法做了兩件事:

  1. providers是一個Map結構的連結串列LinkedHashMap,專門儲存服務例項(在這裡是儲存UserService介面實現類物件)的集合,透過clear()方法做了清除,即清空了裡面的所有記錄。
  2. LazyIterator實現了Iterator迭代器介面,根據類名可以看出,這是一個Lazy懶載入形式的迭代器。

需要額外解釋一下延遲載入是什麼意思。延遲載入,說明專案啟動時不會立馬載入,而是需要被用到的時候,才會動態去載入。實現了Iterator迭代器介面的LazyIterator物件,就具備延遲載入的功能。

簡單看一下,該LazyIterator的結構——

private class LazyIterator implements Iterator<S>
{
    //儲存服務介面的Class型別
    Class<S> service;
    //儲存類載入器。
    ClassLoader loader;
    //儲存服務介面全類名所對應在META-INF.services目錄中的配置檔案資源路徑
    Enumeration<URL> configs = null;
    //儲存裡配置檔案中服務類名的迭代器
    Iterator<String> pending = null;
    //儲存下一個返回的服務提供者類名
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
    ......
 }

總結這部分原始碼,主要是建立一個可載入介面服務提供者例項的ServiceLoader類物件,其內部建立一個具有延遲載入功能的迭代器LazyIterator。該LazyIterator迭代器能夠延遲去逐行遍歷解析出介面全類名所對應配置檔案中的Class類名字串,再將Class類名字串透過反射生成服務提供者物件,儲存到連結串列,用於外部迭代遍歷。

接下來,會基於該延遲載入LazyIterator迭代器,做進一步處理。

到目前為止,只是在ServiceLoader類物件的內部,建立了一個儲存介面UserService.class,類載入器loader的LazyIterator迭代器,暫時還沒涉及到如何獲取介面對應的服務提供者。

簡單理解成,菜刀和鍋都準備好了,就等切菜和煮菜了。

二、呼叫serviceLoader.iterator()返回一個內部實際是呼叫LazyIterator迭代器的匿名迭代器物件

這裡透過serviceLoader.iterator()得到了一個型別為UserService的迭代器。

Iterator<UserService> serviceIterator = serviceLoader.iterator();

先進入到serviceLoader.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();
        }

    };
}

該方法裡,return new Iterator() { ... }表示建立一個實現了Iterator介面的匿名內部類例項物件,並返回該例項物件作為一個迭代器。

至於這個匿名物件是叫張三還是李四,都不重要。重要的是,其內部具有能被外部正常呼叫的hasNext()和next()就可以了。

我畫了一幅簡單的漫畫,舉例說明一下,這裡為何可以直接返回一個實現Iterator介面的匿名內部類例項物件。

故事是這樣的,有一個老闆,想要招一個工具人,哦,不對,是打工人(反正都一樣......)——
image
image
image

故事到這裡就結束了,這個return new Iterator() { ... }返回的匿名內部類,就像無數籍籍無名的底層打工人一樣,或許自始自終都無人知道他們的名字,但他們用自己辛勤的手(hasNext()方法)腳(next()方法),在平凡的崗位上,默默做著不平凡的工作,提供著可以幫助其他人(服務使用者)的服務。

接下來,讓我們看看這些打工人那佈滿皺紋的手和腳——

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

knownProviders是一個包裝了LinkedHashMap providers = new LinkedHashMap<>()連結串列的迭代器。

當呼叫hasNext()或者next()時,都會判斷providers裡是否還有可以遍歷獲取的值,如果空了,就會呼叫lookupIterator.hasNext()或者lookupIterator.next()。

這個lookupIterator,正是前文建立的LazyIterator迭代器物件的引用。

匿名迭代器物件中的這兩個方法,分別是以下兩種功能:

  • hasNext()判斷迭代器是否存在下一個元素。
  • next()獲取迭代器中的下一個元素。

可見,這部分原始碼呼叫serviceLoader.iterator()返回一個提供hasNext()和next()方法的匿名迭代器物件,實際上,hasNext()和next()方法內真實呼叫的是迭代器LazyIterator的hasNext()和next()方法。

三、遍歷迭代器,逐行解析介面全類名所對應配置檔案中的service實現類的名字,透過反射生成物件快取到連結串列,最後返回。

該分析最後的程式碼了,這裡已經到遍歷迴圈迭代器,透過serviceIterator.next()取出儲存介面服務提供者物件——

while (serviceIterator.hasNext()) {
    UserService service = serviceIterator.next();
    service.getName();
    }
}

這裡的hasNext()和next(),正是前文return new Iterator() { ... }匿名物件裡的hasNext()和next()方法。故而在執行serviceIterator.hasNext()或者serviceIterator.next(),將跳轉到#ServiceLoader類#iterator() 中,執行該匿名內部類的hasNext()和next()方法。

先來看hasNext()方法——

public boolean hasNext() {
    if (knownProviders.hasNext())
        return true;
    return lookupIterator.hasNext();
}

若是第一次執行時,knownProviders迭代器裡的LinkedHashMap連結串列必定是空的,這時候,就會執行lookupIterator.hasNext()——

public boolean hasNext() {
    if (acc == null) {
    //acc為空,執行的是這一步程式碼
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

這裡acc為空,故而執行的是return hasNextService()語句——

private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    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;
}

初次呼叫,configs是null,而類載入器loader非空,故而會執行configs = loader.getResources(fullName)這行程式碼。

基於該執行步驟,分析一下這裡的configs作用是什麼,先看以下兩個邏輯——

  1. PREFIX的值為private static final String PREFIX = "META-INF/services/",表示正是目錄META-INF/services/路徑。
  2. service.getName()是獲取Class的name值,我們傳進來的是UserService.class,故而這裡service.getName()獲取到的,便是介面全名com.zhu.service.UserService。

兩者結合,即程式碼String fullName = PREFIX + service.getName()得到的,便是“METAINF/services/com.zhu.service.UserService”字串,表示檔案路徑名。

這時候,我們的類載入器就開始派上用場了——

configs = loader.getResources(fullName);

沒錯,到這裡已經拿到UserService介面全類名對應的檔案路徑,就可以透過類載入器讀取到該檔案資源了。

讀取到該檔案之後,之後就可以解析存放在檔案裡的介面的服務實現類資訊了,故而具體實現在pending =parse(service, configs.nextElement())這行程式碼裡——

while ((pending == null) || !pending.hasNext()) {
    if (!configs.hasMoreElements()) {
        return false;
    }
    //逐行解析讀取配置檔案類名,將讀取到的類名儲存到ArrayList,最後包裝成iterator返回賦值給pending
    pending = parse(service, configs.nextElement());
}

進入到parse方法裡,可以看到,這裡開始透過while((lc =parseLine(service, u, r, lc, names))>=0)對檔案內容逐行讀取,同時建立一個ArrayList names,用來快取讀取出來的類名,具體實現就在parseLine(service, u, r, lc, names))方法裡——

private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
{
    InputStream in = null;
    BufferedReader r = null;
    //用來快取從檔案裡讀取出來的類名
    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);
        }
    }
    //將ArrayList包裝成迭代器返回
    return names.iterator();
}

進入到parseLine(service, u, r, lc, names))方法,程式碼String ln = r.readLine()表示讀取出檔案每一行的字串賦值給ln。

若遇到有#註釋符號的就跳過,只讀取非#號註釋的類名字串,以names.add(ln)儲存到一個ArrayList裡。

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

    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    //過濾掉帶有#字元的
    if (n != 0) {
        if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
            fail(service, u, lc, "Illegal configuration-file syntax");
        int cp = ln.codePointAt(0);
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        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);
        }
        //讀取檔案裡的類名字串儲存到names這個ArrayList裡
        if (!providers.containsKey(ln) && !names.contains(ln))
            names.add(ln);
    }
    return lc + 1;
}

將讀取檔案裡的類名存到ArrayList後,最後return names.iterator()返回一個iterator迭代器,可debug列印看一下,可以看到該ArrayList快取了從檔案裡讀取出來的類名——
image

該迭代器在解析完成後,會執行一次nextName = pending.next(),表示透過迭代器方式取出ArrayList中的第一個字串,即“com.zhu.service.impl.AUserServiceImpl”,同時return true。

image

這裡nextName = pending.next()和return true就呼應了外部服務使用者的呼叫,可見serviceIterator.hasNext()內部,若迭代器下一個元素不為空,那麼就將下一個元素透過取出,賦值給nextName,同時返回true,讓while迴圈正常遍歷下去——
image

前面的nextName = pending.next()將會在serviceIterator.next()裡有所體現。

接下來,在next()中,第一次呼叫,也是lookupIterator.next()方法——

public S next() {
    if (knownProviders.hasNext())
        return knownProviders.next().getValue();
    return lookupIterator.next();
}

進入到lookupIterator.next()方法——

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

同樣,實現的是nextService()——

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        /**
        *nextName即將前文的com.zhu.service.impl.AUserServiceImpl
        *String cn = nextName
        *透過Class.forName(cn, false, loader),即可生成AUserServiceImpl的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 {
        //既然已經拿到AUserServiceImpl的Class類物件,透過反射c.newInstance()便能生成相應物件
        S p = service.cast(c.newInstance());
        //生成的物件會以結構為<實現類名, 實現類物件>的Map形式,儲存到LinkedHashMap連結串列裡
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

在這裡面,主要做了這樣幾件事:

  1. 將nextName字串賦值給cn,首次呼叫時,這裡的nextName值為“com.zhu.service.impl.AUserServiceImpl”;
  2. 透過 c = Class.forName(cn,false, loader)生成AUserServiceImpl類的Class物件;
  3. 透過反射透過c.newInstance()生成AUserServiceImpl類例項物件;
  4. 生成的物件會以結構為<實現類名, 實現類物件>的Map形式,儲存到LinkedHashMap連結串列裡;
  5. 將生成的物件返回;

因此,在第一次呼叫完UserService service = serviceIterator.next()後,就能拿到了介面UserService的第一個實現類物件com.zhu.service.impl.AUserServiceImpl,進而就可以執行相應的重寫方法service.getName()。

到while的第二次遍歷時,執行serviceIterator.hasNext()後,會取出ArrayList中的第二個快取類名“com.zhu.service.impl.BUserServiceImpl”賦值給nextName,這樣在執行UserService service = serviceIterator.next()時,就會重複執行nextService()裡的邏輯。一直迭代遍歷,直到將配置裡的類名都遍歷完,serviceIterator才最終結束該UserService介面的服務提供功能。

首次呼叫就是以上流程,值得提的一個地方是,在反射建立完成的物件後,將以結構為<實現類名, 實現類物件>的Map形式。儲存到LinkedHashMap連結串列裡。

這個LinkedHashMap連結串列快取的作用是什麼呢?

這時回頭去看下這行程式碼,還記得它裡面建立了一個匿名內部類嗎——
image

這個匿名內部類裡,其hasNext()和next()方法,會判斷knownProviders是否為空,不為空才去呼叫knownProviders裡的方法。

這裡的knownProviders正是使用到了LinkedHashMap連結串列快取裡的物件。
image

這個連結串列的作用,就是方便出現重複建立一個匿名迭代器去後去獲取介面的服務物件時,直接從LinkedHashMap連結串列快取裡讀取即可,無需再次去解析介面對應的配置檔案,起到了查詢最佳化的作用。

類似這樣的場景,第二次生成一個迭代器去提供介面的服務功能時,就直接從從LinkedHashMap連結串列快取裡讀取了。
image

以上,就是Java SPI的完整原始碼分析。

相關文章