java SPI 03-ServiceLoader jdk 源碼解析

系列目錄

spi 01-spi 是什麼?入門使用html

spi 02-spi 的實戰解決 slf4j 包衝突問題java

spi 03-spi jdk 實現源碼解析mysql

spi 04-spi dubbo 實現源碼解析git

spi 05-dubbo adaptive extension 自適應拓展github

spi 06-本身從零手寫實現 SPI 框架sql

spi 07-自動生成 SPI 配置文件實現方式數據庫

java SPI 加載流程

1 應用程序調用ServiceLoader.load方法

ServiceLoader.load方法內先建立一個新的ServiceLoader,並實例化該類中的成員變量,包括:緩存

loader(ClassLoader類型,類加載器)
acc(AccessControlContext類型,訪問控制器)
providers(LinkedHashMap<String,S>類型,用於緩存加載成功的類)
lookupIterator(實現迭代器功能)

2 應用程序經過迭代器接口獲取對象實例

ServiceLoader 先判斷成員變量 providers 對象中( LinkedHashMap&lt;String,S&gt; 類型)是否有緩存實例對象,若是有緩存,直接返回。安全

若是沒有緩存,執行類的裝載,實現以下:多線程

(1) 讀取META-INF/services/下的配置文件,得到全部能被實例化的類的名稱,值得注意的是,ServiceLoader能夠跨越jar包獲取META-INF下的配置文件,具體加載配置的實現代碼以下:

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

(2) 經過反射方法 Class.forName() 加載類對象,並用 instance() 方法將類實例化。

(3) 把實例化後的類緩存到 providers 對象中,( LinkedHashMap&lt;String,S&gt; 類型,而後返回實例對象。

看到這裏,實際上對咱們理解 SPI 的標準頗有幫助,好比爲何須要無參構造器。

java SPI 源碼

下面咱們簡單的總體過一遍源碼。

java.util 包下。

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

從註釋中可知該類是 jdk1.6 開始支持,繼承自 Iterable。

私有變量

PREFIX 對應的就是咱們指定 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;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

方法入口

咱們回憶一下使用時的方式:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ServiceLoader<Say> loader = ServiceLoader.load(Say.class, classLoader);

for (Say say : loader) {
    say.say();
}

這裏也就展現了方法的入口,獲取當前的 ClassLoader,咱們來看一下 load 方法。

load

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

直接建立了一個實例,這個構造器方法是 private 的:

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();
}
  • load() 重載

爲了提供遍歷,提供了默認的 ClassLoader 實現

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

這裏作了從新加載,清空了 providers,而且新建了一個 LazyIterator。

private LinkedHashMap&lt;String,S&gt; providers = new LinkedHashMap&lt;&gt;(); 其實就是一個 cache,每次初始化會作清空。

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

LazyIterator

咱們來看一下這個迭代器的實現

源碼

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

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

其中最核心的兩個方法 hasNextService() 會去指定的目錄下處理配置信息,hasNextService() 有以下核心實現。

c = Class.forName(cn, false, loader);

// 建立而且緩存
S p = service.cast(c.newInstance());
providers.put(cn, p);

parse

hasNextService() 方法中的 parse 也值得看一下。

實際就是去解析文件夾下的配置文件,按照行讀取。

能夠看出來,默認使用的是 utf-8 文件編碼。

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);
        }
    }
    return names.iterator();
}

fail()

這個方法出現了屢次,實際上只是一個報錯信息,知道便可:

private static void fail(Class<?> service, String msg)
    throws ServiceConfigurationError
{
    throw new ServiceConfigurationError(service.getName() + ": " + msg);
}

獲取值

這裏是遍歷了 loader,實際上就是 java 的一個語法糖。

對應的是 Iterator, 對應的 hasNext() 和 next() 方法。

for (Say say : loader) {
    say.say();
}

這裏就是遍歷了 cache 中的實現,至於 cache 中的信息怎麼來的,就是 LazyIterator 遍歷過程當中的建立實例+cache。

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

小結

但就源碼而言,實現機制並不複雜。

可是思想比較不錯,也帶了很大的方便。

當咱們看了源碼以後,對於優缺點實際會有更加清晰的認識。

優缺點

優勢

使用Java SPI機制的優點是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一塊兒。

應用程序能夠根據實際業務狀況啓用框架擴展或替換框架組件。

相比使用提供接口jar包,供第三方服務模塊實現接口的方式,SPI的方式使得源框架,沒必要關心接口的實現類的路徑,能夠不用經過下面的方式獲取接口實現類:

  1. 代碼硬編碼import 導入實現類

  2. 指定類全路徑反射獲取:例如在JDBC4.0以前,JDBC中獲取數據庫驅動類須要經過 Class.forName("com.mysql.jdbc.Driver"),相似語句先動態加載數據庫相關的驅動,而後再進行獲取鏈接等的操做

  3. 第三方服務模塊把接口實現類實例註冊到指定地方,源框架從該處訪問實例

經過SPI的方式,第三方服務模塊實現接口後,在第三方的項目代碼的 META-INF/services 目錄下的配置文件指定實現類的全路徑名,源碼框架便可找到實現類

缺點

  1. 雖然ServiceLoader也算是使用的延遲加載,可是基本只能經過遍歷所有獲取,也就是接口的實現類所有加載並實例化一遍。若是你並不想用某些實現類,它也被加載並實例化了,這就形成了浪費。

  2. 獲取某個實現類的方式不夠靈活,只能經過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。

  3. 多個併發多線程使用ServiceLoader類的實例是不安全的。

後續

實際上 hibernate-validator/dubbo 等常見框架,都有用到 SPI。

後續咱們將一塊兒來看一下 dubbo 中的實現,看看 dubbo 是如何解決這些不足之處的。

參考資料

深刻理解 SPI 機制

高級開發必須理解的 Java 中 SPI 機制

相關文章
相關標籤/搜索