經過源碼淺析Java中的資源加載

前提

最近在作一個基礎組件項目恰好須要用到JDK中的資源加載,這裏說到的資源包括類文件和其餘靜態資源,恰好須要從新補充一下類加載器和資源加載的相關知識,整理成一篇文章。java

理解類的工做原理

這一節主要分析類加載器和雙親委派模型。面試

什麼是類加載器

虛擬機設計團隊把類加載階段中的"經過一個類的全限定名來獲取描述此類的二進制字節流"這個動做放到了Java虛擬機外部實現,以便讓應用程序本身決定如何去獲取所須要的類,而實現這個動做的代碼模塊稱爲"類加載器(ClassLoader)"。編程

類加載器雖然只用於實現類加載的功能,可是它在Java程序中起到的做用不侷限於類加載階段。對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立類在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類命名空間。上面這句話直觀來講就是:比較兩個類是否"相等",只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這個兩個類是來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那麼這兩個類必然"不相等"。這裏說到的"相等"包括表明類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceOf關鍵字作對象所屬關係斷定等狀況。bootstrap

類和加載它的類加載器肯定類在Java虛擬機中的惟一性這個特色爲後來出現的熱更新類、熱部署等技術提供了基礎。api

雙親委派模型

從Java虛擬機的角度來看,只有兩種不一樣的類加載器:數組

  • 一、第一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++編程語言實現,是虛擬機的一部分。
  • 二、另外一種是其餘的類加載器,這些類加載器都是由Java語言實現,獨立於虛擬機以外,通常就是內部於JDK中,它們都繼承自抽象類加載器java.lang.ClassLoader。

JDK中提供幾個系統級別的類加載器:緩存

  • 一、啓動類加載器(Bootstrap ClassLoader):這個類加載器負責將存放在${JAVA_HONE}\lib目錄中,或者被XbootstrapPath參數所指定的目錄中,而且是虛擬機基於必定規則(如文件名稱規則,如rt.jar)標識的類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,開發者在編寫自定義類加載器若是想委派到啓動類加載器只需直接使用null替代便可。
  • 二、擴展類加載器(Extension ClassLoader):這個類加載器由sun.misc.Launcher的靜態內部類ExtClassLoader實現,它負責加載${JAVA_HONE}\lib\ext目錄中,或者經過java.ext.dirs系統變量指定的路徑中的全部類庫,開發者能夠直接使用此類加載器。
  • 三、應用程序類加載器(Application ClassLoader):這個類加載器由sun.misc.Launcher的靜態內部類AppClassLoader實現,可是因爲這個類加載器的實例是ClassLoader中靜態方法getSystemClassLoader()中的返回值,通常也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過自實現的類加載器,通常狀況下這個系統類加載器就是應用程序中默認使用的類加載器。
  • 四、線程上下文類加載器(Thread Context ClassLoader):這個在下一小節"破壞雙親委派模型"再分析。

Java開發者開發出來的Java應用程序都是由上面四種類加載器相互配合進行類加載的,若是有必要還能夠加入自定義的類加載器。其中,啓動類加載器、擴展類加載器、應用程序類加載器和自定義類加載器之間存在着必定的關係:編程語言

r-l-1

上圖展現的類加載器之間的層次關係稱爲雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的類加載器(Java中頂層的類加載器通常是Bootstrap ClassLoader),其餘的類加載器都應當有本身的父類加載器。這些類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是經過組合(Composition)的關係實現。類加載器層次關係這一點能夠經過下面的代碼驗證一下:ide

public class Main {

    public static void main(String[] args) throws Exception{
        ClassLoader classLoader = Main.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }
}

//輸出結果,最後的null說明是Bootstrap ClassLoader
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4629104a
null

雙親委派模型的工做機制:若是一個類加載器收到了類加載的請求,它首先不會本身嘗試去加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的類加載請求最終都應該傳送到頂層的類加載器中,只有當父類加載器反饋本身沒法完成當前的類加載請求的時候(也就是在它的搜索範圍中沒有找到所須要的類),子類加載器纔會嘗試本身去加載類。不過這裏有一點須要注意,每個類加載器都會緩存已經加載過的類,也就是重複加載一個已經存在的類,那麼就會從已經加載的緩存中加載,若是從當前類加載的緩存中判斷類已經加載過,那麼直接返回,不然會委派類加載請求到父類加載器。這個緩存機制在AppClassLoader和ExtensionClassLoader中都存在,至於BootstrapClassLoader未知。工具

r-l-2

雙親委派模型的優點:使用雙親委派模型來組織類加載器之間的關係,一個比較顯著的優勢是Java類隨着加載它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如java.lang包中的類庫,它存放在rt.jar中,不管使用哪個類加載加載java.lang包中的類,最終都是委派給處於模型頂層的啓動類加載器進行加載,所以java.lang包中的類如java.lang.Object類在應用程序中的各種加載器環境中加載的都是同一個類。試想,若是可使用用戶自定義的ClassLoader去加載java.lang.Object,那麼用戶應用程序中就會出現多個java.lang.Object類,Java類型體系中最基礎的類型也有多個,類型體系的基礎行爲沒法保證,應用程序也會趨於混亂。若是嘗試編寫rt.jar中已經存在的同類名的類經過自定義的類加載進行加載,將會接收到虛擬機拋出的異常。

雙親委派模型的實現:類加載器雙親委派模型的實現提如今ClassLoader的源碼中,主要是ClassLoader#loadClass()中。

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //父加載器不爲null,說明父加載器不是BootstrapClassLoader
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    //父加載器爲null,說明父加載器是BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            //全部的父加載加載失敗,則使用當前的類加載器進行類加載
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);
                //記錄一些統計數據如加載耗時、計數等
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
     }
}

破壞雙親委派模型

雙親委派模型在Java發展歷史上出現了三次比較大"被破壞"的狀況:

  • 一、ClassLoader在JDK1.0已經存在,JDK1.2爲了引入雙親委派模型而且須要向前兼容,java.lang.ClassLoader類添加了一個新的protected的findClass()方法,在這以前,用戶去繼承java.lang.ClassLoader只能重寫其loadClass()方法才能實現本身的目標。

  • 二、雙親委派模型自身存在缺陷:雙親委派很好地解決了各個類加載器的基礎類的加載的統一問題(越基礎的類由越上層的類加載器加載),這些所謂的基礎類就是大多數狀況下做爲用戶調用的基礎類庫和基礎API,可是沒法解決這些基礎類須要回調用戶的代碼這一個問題,典型的例子就是JNDI。JNDI的類庫代碼是啓動類加載器加載的,可是它須要調用獨立廠商實現而且部署在應用的ClassPath的JNDI的服務接口提供者(SPI,便是Service Provider Interface)的代碼,可是啓動類加載器沒法加載ClassPath下的類庫。爲了解決這個問題,Java設計團隊引入了不優雅的設計:線程上下文類加載器(Thread Context ClassLoader),這個類加載器能夠經過java.lang.Thread類的setContextClassLoader()設置,這樣子,JNDI服務就可使用線程上下文類加載器去加載所需的SPI類庫,可是父類加載器中請求子類加載器去加載類這一點已經打破了雙親委派模型。目前,JNDI、JDBC、JCE、JAXB和JBI等模塊都是經過此方式實現。

  • 三、基於用戶對應用程序動態性的熱切追求:如代碼熱替換(HotSwap)、熱模塊部署等,說白了就是但願應用程序能像咱們的計算機外設那樣能夠熱插拔,所以催生出JSR-291以及它的業界實現OSGi,而OSGi定製了本身的類加載規則,再也不遵循雙親委派模型,所以它能夠經過自定義的類加載器機制輕易實現模塊的熱部署。

JDK中提供的資源加載API

前邊花大量的篇幅去分析類加載器的預熱知識,是由於JDK中的資源加載依賴於類加載器(其實類文件原本就是資源文件的一種,類加載的過程也是資源加載的過程)。這裏先列舉出JDK中目前經常使用的資源(Resource)加載的API,先看ClassLoader中提供的方法。

ClassLoader提供的資源加載API

//1.實例方法

public URL getResource(String name)

//這個方法僅僅是調用getResource(String name)返回URL實例直接調用URL實例的openStream()方法
public InputStream getResourceAsStream(String name)

//這個方法是getResource(String name)方法的複數版本
public Enumeration<URL> getResources(String name) throws IOException

//2.靜態方法

public static URL getSystemResource(String name)

//這個方法僅僅是調用getSystemResource(String name)返回URL實例直接調用URL實例的openStream()方法
public static InputStream getSystemResourceAsStream(String name)

//這個方法是getSystemResources(String name)方法的複數版本
public static Enumeration<URL> getSystemResources(String name)

總的來看,只有兩個方法須要分析:getResource(String name)getSystemResource(String name)。查看getResource(String name)的源碼:

public URL getResource(String name) {
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = getBootstrapResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}

是否似曾相識?這裏明顯就是使用了類加載過程當中相似的雙親委派模型進行資源加載,這個方法在API註釋中描述一般用於加載數據資源如images、audio、text等等,資源名稱須要使用路徑分隔符'/'。getResource(String name)方法中查找的根路徑咱們能夠經過下面方法驗證:

public class ResourceLoader {

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = ResourceLoader.class.getClassLoader();
        URL resource = classLoader.getResource("");
        System.out.println(resource);
    }
}

//輸出:file:/D:/Projects/rxjava-seed/target/classes/

很明顯輸出的結果就是當前應用的ClassPath,總結來講:ClassLoader#getResource(String name)是基於用戶應用程序的ClassPath搜索資源,資源名稱必須使用路徑分隔符'/'去分隔目錄,可是不能以'/'做爲資源名的起始,也就是不能這樣使用:classLoader.getResource("/img/doge.jpg")。接着咱們再看一下ClassLoader#getSystemResource(String name)的源碼:

public static URL getSystemResource(String name) {
    //實際上Application ClassLoader通常不會爲null
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        return getBootstrapResource(name);
    }
    return system.getResource(name);
}

此方法優先使用應用程序類加載器進行資源加載,若是應用程序類加載器爲null(其實這種狀況不多見),則使用啓動類加載器進行資源加載。若是應用程序類加載器不爲null的狀況下,它實際上退化爲ClassLoader#getResource(String name)方法。

總結一下:ClassLoader提供的資源加載的方法中的核心方法是ClassLoader#getResource(String name),它是基於用戶應用程序的ClassPath搜索資源,遵循"資源加載的雙親委派模型",資源名稱必須使用路徑分隔符'/'去分隔目錄,可是不能以'/'做爲資源名的起始字符,其餘幾個方法都是基於此方法進行衍生,添加複數操做等其餘操做。getResource(String name)方法不會顯示拋出異常,當資源搜索失敗的時候,會返回null。

Class提供的資源加載API

java.lang.Class中也提供了資源加載的方法,以下:

public java.net.URL getResource(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResource(name);
    }
    return cl.getResource(name);
}

public InputStream getResourceAsStream(String name) {
    name = resolveName(name);
    ClassLoader cl = getClassLoader0();
    if (cl==null) {
        // A system class.
        return ClassLoader.getSystemResourceAsStream(name);
    }
    return cl.getResourceAsStream(name);
}

從上面的源碼來看,Class#getResource(String name)Class#getResourceAsStream(String name)分別比ClassLoader#getResource(String name)ClassLoader#getResourceAsStream(String name)只多了一步,就是搜索以前先進行資源名稱的預處理resolveName(name),咱們重點看這個方法作了什麼:

private String resolveName(String name) {
    if (name == null) {
        return name;
    }
    if (!name.startsWith("/")) {
        Class<?> c = this;
        while (c.isArray()) {
            c = c.getComponentType();
        }
        String baseName = c.getName();
        int index = baseName.lastIndexOf('.');
        if (index != -1) {
            name = baseName.substring(0, index).replace('.', '/')
                    +"/"+name;
         }
    } else {
         name = name.substring(1);
    }
    return name;
}

邏輯相對比較簡單:

  • 一、若是資源名稱以'/'開頭,那麼直接去掉'/',這個時候的資源查找實際上退化爲ClassPath中的資源查找。
  • 二、若是資源名稱不以'/'開頭,那麼解析出當前類的實際類型(由於當前類有多是數組),取出類型的包路徑,替換包路徑中的'.'爲'/',再拼接原來的資源名稱。舉個例子:"club.throwable.Main.class"中調用了Main.class.getResource("doge.jpg"),那麼這個調用的處理資源名稱的結果就是club/throwable/doge.jpg

小結:若是看過我以前寫過的一篇URL和URI相關的文章就清楚,實際上Class#getResource(String name)Class#getResourceAsStream(String name)的資源名稱處理相似於相對URL的處理,而"相對URL的處理"的根路徑就是應用程序的ClassPath。若是資源名稱以'/'開頭,那麼至關於從ClassPath中加載資源,若是資源名稱不以'/'開頭,那麼至關於基於當前類的實際類型的包目錄下加載資源。

實際上相似這樣的資源加載方式在File類中也存在,這裏就再也不展開。

小結

理解JDK中的資源加載方式有助於編寫一些通用的基礎組件,像Spring裏面的ResourceLoader、ClassPathResource這裏比較實用的工具也是基於JDK資源加載的方式編寫出來。下一篇博文《淺析JDK中ServiceLoader的源碼》中的主角ServiceLoader就是基於類加載器的功能實現,它也是SPI中的服務類加載的核心類。

說實話,類加載器的"雙親委派模型"和"破壞雙親委派模型"是常見的面試題相關內容,這裏能夠簡單列舉兩個面試題:

  • 一、談談對類加載器的"雙親委派模型"的理解。
  • 二、爲何要引入線程上下文類加載器(或者是對於問題1有打破這個模型的案例嗎)?

但願這篇文章能幫助你理解和解決這兩個問題。

參考資料:

  • 《深刻理解Java虛擬機第二版》
  • JavaSE-8源碼

(本文完 c-1-d e-20181014)

相關文章
相關標籤/搜索