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


資源包括類文件和其他靜態資源。

核心方法classLoader.getResource

JDK中提供的資源加載API

ClassLoader提供的資源加載API

//1.實例方法

public URL getResource(String name)

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

//這個方法是getResource(String name)方法的複數版本,返回匹配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)的源碼:

//java.lang.ClassLoader.getResource方法
public URL getResource(String name) {
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = getBootstrapResource(name);
    }
    if (url == null) {
        url = findResource(name);//java.net.URLClassLoader.findResource
    }
    return url;
}

這裏明顯就是使用了類加載過程中類似的雙親委派模型進行資源加載,這個方法在API註釋中描述通常用於加載數據資源如images、audio、text等等,資源名稱路徑分隔需要使用路徑分隔符/, 但是資源路徑不能使用/字符開始。
getResource(String name)方法中查找的根路徑我們可以通過下面方法驗證:

ClassLoader classLoader = ResourceLoader.class.getClassLoader();
URL resource = classLoader.getResource("");
System.out.println(resource);
//輸出:路徑是 classpath 
//file:/D:/Dev/WorkStation/Multthreadinaction/target/classes/

輸出的結果就是當前應用的ClassPath,總結來說:ClassLoader.getResource(String name)是基於用戶應用程序的ClassPath搜索資源,資源名稱必須使用路徑分隔符’/‘去分隔目錄,但是不能以’/'作爲資源名的起始.

getResource方法調用 findResource 方法用於定義從哪裏去尋找資源, 每個classloader 都要覆蓋實現 findResource 方法以指定在哪裏尋找資源。

默認情況下 沒有第三方實現的classloader去加載classpath資源,findResource方法只有一個java.net.URLClassLoader.findResource實現

//java.net.URLClassLoader.findResource
public URL findResource(final String name) {
    /*
     * The same restriction to finding classes applies to resources
     */
    URL url = AccessController.doPrivileged(
        new PrivilegedAction<URL>() {
            public URL run() {
            //從ucp.path目錄中包含所有可能的classpath路徑,
            //ucp.findResource遍歷ucp所有 loaders 屬性和name參數嘗試拼接在一起
                return ucp.findResource(name, true);
            }
        }, acc);

    return url != null ? ucp.checkURL(url) : null;
}

遍歷ucp 所有 loaders 屬性

//sun.misc.URLClassPath.findResource
//參數是: name, true
public URL findResource(String var1, boolean var2) {
    int[] var4 = this.getLookupCache(var1);

    URLClassPath.Loader var3;
    // 遍歷ucp.loaders 屬性, 對每個 Loader 有 base,屬性,這個屬性記錄了classpath的絕對路徑存儲在base屬性。
    for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
        URL var6 = var3.findResource(var1, var2);//var1 =name,var2 = true
        if (var6 != null) {
            return var6;
        }
    }

    return null;
}

在var3.findResource方法中:

//sun.misc.URLClassPath.Loader.findResource 內部類,對每個Loader.base + name屬性拼接起來URL 檢查是否可以 打開鏈接。
//參數是: name, true
URL findResource(String var1, boolean var2) {
        URL var3;
        try {
        // base 就是記錄的classpath 絕對路徑屬性URL, var1就是相對於classpath的相對路徑,拼接起來如果
            var3 = new URL(this.base, ParseUtil.encodePath(var1, false));
        } catch (MalformedURLException var7) {
            throw new IllegalArgumentException("name");
        }
        URLConnection var4 = var3.openConnection(); //如果能返回連接說明文件資源真實存在,查找到了。
        ...
        return var3;//返回資源URL鏈接

getSystemResource 源碼(靜態方法)

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

public java.net.URL getResource(String name) {
    name = resolveName(name);
    //獲取的是ApplicationClassLoader
    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);
    //獲取的是ApplicationClassLoader
    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路徑中的資源查找。
  • 如果資源名稱不以’/‘開頭,那麼解析出當前類的實際類型(因爲當前類有可能是數組),取出類型的包全路徑,替換包路徑中的’.‘爲’/’,再拼接原來的資源名稱。舉個例子:org.vincent.res.ResourceLoader.class中調用了ResourceLoader.class.getResource("123.md"),那麼這個調用的處理資源名稱的結果就是org/vincent/res/123.md
    類似這樣的資源加載方式在File類中也存在 。

測試類

package org.vincent.res;

import java.net.URL;

/**
 * @author PengRong
 * @package org.vincent.res
 * @ClassName ResourceLoader.java
 * @date 2019/3/25 - 7:20
 * @ProjectName Multthread-in-action
 * @Description: JDK 資源加載
 */
public class ResourceLoader {
    public static void main(String[] args) {

        ClassLoader classLoader = ResourceLoader.class.getClassLoader();

        URL resource = classLoader.getResource("spring/123.md");
        System.out.println(resource);
        resource = classLoader.getResource("");
        System.out.println(resource);
        resource = classLoader.getSystemResource("spring/123.md");
        System.out.println(resource);
        /** classLoader 加載資源路徑 不能以 / 字符開始*/
        resource = classLoader.getResource("/spring/123.md");
        System.out.println(resource);

        /** Class類加載資源文件:可以以 / 字符開始,那麼他的 實際尋找路徑被resolveName方法處理後變成classpath下 spring/123.md*/
        resource = ResourceLoader.class.getResource("/spring/123.md");
        System.out.println(resource);
        /** Class類加載資源文件: 可以不以 / 字符開始,那麼被resolveName方法處理後 實際尋找路徑也是classpath下,只是會增加上當前類package全路徑 org/vincent/res/123.md*/
        resource = ResourceLoader.class.getResource("123.md");
    }
}

參考

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章