通过源码浅析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中的资源加载

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