转载

深入了解 Java Resource && Spring Resource

Java 中,为了从相对路径读取文件,经常会使用的方法便是:

xxx.class.getResource();

xxx.class.getClassLoader().getResource();
复制代码

Spring 中,我们还可以通过 Spring 提供的 Resource 进行一些操作:

ClassPathResource

FileSystemResource

ServletContextResource

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
复制代码

这里简单总结下他们的区别:

ClassLoader##getResource()

这个方法是今天的主角。

我们都知道 ClassLoader 的作用是用来加载 .class 文件的,并且 ClassLoader 是遵循 Java 类加载中的双亲委派机制的。

那么, ClassLoader 是如何找到这个 .class 文件的呢?答案是 URLClassPath

Java 中自带了3个 ClassLoader 分别是 BootStrap ClassLoaderEtxClassLoader , AppClassLoader ,

这3个 ClassLoader 都继承自 URLClassLoader ,而 URLClassLoader 中包含一个 URLClassPath 用来记录每个 ClassLoader 对应的加载 .class 文件的路径,当需要加载资源的时候,只管从 URLClassPath 对应的路径查找即可。

下面是测试代码:

System.out.println("BootStrap ClassLoader ");
Stream.of(System.getProperty("sun.boot.class.path").split(";")).forEach(System.out::println);

System.out.println("ExtClassLoader:");  
Stream.of(System.getProperty("java.ext.dirs").split(";")).forEach(System.out::println);

System.out.println("AppClassLoader:");  
Stream.of(System.getProperty("java.class.path").split(";")).forEach(System.out::println);
复制代码

输出如下:

BootStrap ClassLoader 

H:/java/jdk1.8/jre/lib/resources.jar
H:/java/jdk1.8/jre/lib/rt.jar
H:/java/jdk1.8/jre/lib/sunrsasign.jar
H:/java/jdk1.8/jre/lib/jsse.jar
H:/java/jdk1.8/jre/lib/jce.jar
H:/java/jdk1.8/jre/lib/charsets.jar
H:/java/jdk1.8/jre/lib/jfr.jar
H:/java/jdk1.8/jre/classes

ExtClassLoader:

H:/java/jdk1.8/jre/lib/ext
C:/Windows/Sun/Java/lib/ext

AppClassLoader:

H:/java/jdk1.8/jre/lib/charsets.jar
H:/java/jdk1.8/jre/lib/deploy.jar
H:/java/jdk1.8/jre/lib/ext/access-bridge-64.jar
H:/java/jdk1.8/jre/lib/ext/cldrdata.jar
H:/java/jdk1.8/jre/lib/ext/dnsns.jar
H:/java/jdk1.8/jre/lib/ext/jaccess.jar
H:/java/jdk1.8/jre/lib/ext/jfxrt.jar
H:/java/jdk1.8/jre/lib/ext/localedata.jar
H:/java/jdk1.8/jre/lib/ext/nashorn.jar
H:/java/jdk1.8/jre/lib/ext/sunec.jar
H:/java/jdk1.8/jre/lib/ext/sunjce_provider.jar
H:/java/jdk1.8/jre/lib/ext/sunmscapi.jar
H:/java/jdk1.8/jre/lib/ext/sunpkcs11.jar
H:/java/jdk1.8/jre/lib/ext/zipfs.jar
H:/java/jdk1.8/jre/lib/javaws.jar
H:/java/jdk1.8/jre/lib/jce.jar
H:/java/jdk1.8/jre/lib/jfr.jar
H:/java/jdk1.8/jre/lib/jfxswt.jar
H:/java/jdk1.8/jre/lib/jsse.jar
H:/java/jdk1.8/jre/lib/management-agent.jar
H:/java/jdk1.8/jre/lib/plugin.jar
H:/java/jdk1.8/jre/lib/resources.jar
H:/java/jdk1.8/jre/lib/rt.jar
F:/spring-test/target/classes
复制代码

AppClassLoader 负责常用的 JDK jar 以及项目所依赖的 jar

上述参数可以通过 sun.misc.Launcher.class获得

通过输出的参数,我们可以清晰的看出来各个 ClassLoader 负责的区域

说了这么多,这个和 ClassLoader#getResource() 有什么关系呢?

关系很大,前面刚刚提问过, ClassLoader 是如何读取 .class 文件的呢?

答案是 URLClassPath#getResource() 方法:每个 UrlClassLoader 都是通过 URLClassPath 来存储对应的加载区域,当需要查找 .class 文件的时候,就通过 URLClassPath#getResource() 查找即可。

下面再来看看 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);
        }
        return url;
}

//由于BootStrap ClassLoader是C++写的,Java拿不到其引用。
//因此这里单独写了一个方法获取BootStrapResource()
private static URL getBootstrapResource(String name) {
    URLClassPath ucp = getBootstrapClassPath();
    Resource res = ucp.getResource(name);
    return res != null ? res.getURL() : null;
}

复制代码

URLClassLoader#findResource()

public URL findResource(final String name) {

        URL url = AccessController.doPrivileged(
            new PrivilegedAction<URL>() {
                public URL run() {
                    return ucp.findResource(name, true);
                }
            }, acc);

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

复制代码

我们只用注意这一句 ucp.findResource(name, true); ,这边是查找 .class 文件的方法,因此我们可以总结出通过 ClassLoader#getResource() 的流程:

  • 首先, AppClassLoader 委派给 ExtClassLoader 查找是否存在对应的资源
  • ExtClassLoader 委派给 BootStrap ClassLoader 查找是有存在对应的资源
  • BootStrap ClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回
  • BootStrap ClassLoader 未查找到对应资源, ExtClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回
  • ExtClassLoader 未查找到对应资源, AppClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回
  • AppClassLoader 未查找到,抛出异常。

这个过程,就和加载 .class 文件的过程一样。

在这里我们就可以发现,通过 ClassLoader#getResource() 可以获取 JDK 资源,所依赖的 JAR 包资源等

因此,我们甚至可以这样写:

//读取 java.lang.String.class 的字节码

InputStream inputStream =Test.class.getClassLoader().getResourceAsStream("java/lang/String.class");
try(BufferedInputStream bufferedInputStream=new BufferedInputStream(inputStream)){
    byte[] bytes=new byte[1024];
    while (bufferedInputStream.read(bytes)>0){
        System.out.println(new String(bytes, StandardCharsets.UTF_8));
    }
}
复制代码

明白了 ClassLoader#getResource() ,其实本篇文章就差不多了,因为后面要将的几个方法,底层都是 ClassLoader#getResource()

class##getResource()

class##getResource() 底层就是 ClassLoader#getResource()

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

复制代码

不过有个小区别就在于 class#getResource() 多了一个 resolveName() 方法:

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;
    }
复制代码

这个 resolveName() 大致就是判断路径是相对路径还是绝对路径,如果是相对路径,则资源名会被加上当前项目的根路径:

Test.class.getResource("spring-config.xml");
复制代码

resolve之后变成

com/dengchengchao/test/spring-config.xml
复制代码

这样的资源就只能在当前项目中找到。

Test.class.getResource("test.txt");    //相对路径

Test.class.getResource("/");           //根路径

复制代码

注意: ClassLoader#getResource() 不能以 / 开头

Spring # ClassPathResource()

Spring 中,对 Resource 进行了扩展,使得 Resource 能够适应更多的应用场景,

不过ClssPathResource()底层依然是ClassLoader##getResource(),因此ClassLoader##getResource()d的特性,ClassPathResource也支持。

protected URL resolveURL() {
        if (this.clazz != null) {
            return this.clazz.getResource(this.path);
        } else {
            return this.classLoader != null ? this.classLoader.getResource(this.path) : ClassLoader.getSystemResource(this.path);
        }
    }
复制代码

ClassPathResource 用于读取 classes 目录文件

一般来说,对于 SpringBoot 项目,打包后的项目结构如下:

xxx.jar

|--- BOOT-INF

|--------|--classes

|--------|----|--com

|--------|----|-- application.properties

|--------|----|--logback.xml

| -------|-- lib

|--- META-INF

|--- org

可以看到, ClassPathResource() 的起始路径便是 classes ,平时我们读取的 application.properties 便是使用 ClasspathResource() 获取的

在平时使用的过程中,有三点需要注意:

  1. classpath 和 classpath* 区别:

    classpath:只会返回第一个查找到的文件 classpath*:会返回所有查找到的文件

  2. Spring 中,需要直接表示使用 ClassPathResource() 来查找的话,可以直接添加 classpath:

  3. 使用 classpath/ 和不以 / 开头没有区别

Spring # ServletContextResource

ServletContextResource 是针对 Servlet 来做的,我们知道, Servlet 规定 webapp 目录如下:

深入了解 Java Resource &amp;&amp; Spring Resource

ServletContextResource 的路径则是 xxx 目录下为起点。也就是可以通过 ServletContextResource 获取到 form.html 等资源。

同时对比上面的 ClassPathResource 我们可以发现:

"classpath:com"   
复制代码

等价于:

ServletContextResource("WEB-INF/classes/com")
复制代码

Spring # FileSystemResource

FileSystemResource 没什么好说的,就是系统目录资源,比如

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("D://test.xml");
复制代码

它的标记头为 file:

例如:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("flie:D://test.xml");
复制代码

如果觉得写得不错,欢迎关注微信公众号:逸游Java ,每天不定时发布一些有关Java进阶的文章,感谢关注

深入了解 Java Resource &amp;&amp; Spring Resource
原文  https://juejin.im/post/5dc928196fb9a04a80356004
正文到此结束
Loading...