• 深入了解 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:javajdk1.8jrelib
    esources.jar
    H:javajdk1.8jrelib
    t.jar
    H:javajdk1.8jrelibsunrsasign.jar
    H:javajdk1.8jrelibjsse.jar
    H:javajdk1.8jrelibjce.jar
    H:javajdk1.8jrelibcharsets.jar
    H:javajdk1.8jrelibjfr.jar
    H:javajdk1.8jreclasses
    
    ExtClassLoader:
    
    H:javajdk1.8jrelibext
    C:WindowsSunJavalibext
    
    AppClassLoader:
    
    H:javajdk1.8jrelibcharsets.jar
    H:javajdk1.8jrelibdeploy.jar
    H:javajdk1.8jrelibextaccess-bridge-64.jar
    H:javajdk1.8jrelibextcldrdata.jar
    H:javajdk1.8jrelibextdnsns.jar
    H:javajdk1.8jrelibextjaccess.jar
    H:javajdk1.8jrelibextjfxrt.jar
    H:javajdk1.8jrelibextlocaledata.jar
    H:javajdk1.8jrelibext
    ashorn.jar
    H:javajdk1.8jrelibextsunec.jar
    H:javajdk1.8jrelibextsunjce_provider.jar
    H:javajdk1.8jrelibextsunmscapi.jar
    H:javajdk1.8jrelibextsunpkcs11.jar
    H:javajdk1.8jrelibextzipfs.jar
    H:javajdk1.8jrelibjavaws.jar
    H:javajdk1.8jrelibjce.jar
    H:javajdk1.8jrelibjfr.jar
    H:javajdk1.8jrelibjfxswt.jar
    H:javajdk1.8jrelibjsse.jar
    H:javajdk1.8jrelibmanagement-agent.jar
    H:javajdk1.8jrelibplugin.jar
    H:javajdk1.8jrelib
    esources.jar
    H:javajdk1.8jrelib
    t.jar
    F:spring-test	argetclasses
    

    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目录如下:

    image

    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进阶的文章,感谢关注

  • 相关阅读:
    严重: Parse error in application web.xml file at jndi:/localhost/ipws/WEBINF/web.xml java.lang.NoSuchMethodException: org.apache.catalina.deploy.WebXml
    Failed to install .apk on device 'emulator5554': timeout解决方法
    java.lang.NoClassDefFoundError:org.jsoup.Jsoup
    Conversion to Dalvik format failed: Unable to execute dex:解决方法
    apache Digest: generating secret for digest authentication ...
    Description Resource Path Location Type Project has no default.properties file! Edit the project properties to set one.
    android service随机自启动
    MVC3 安装部署
    EF 4.3 CodeBased 数据迁移演练
    SQL Server 2008开启sa账户
  • 原文地址:https://www.cnblogs.com/dengchengchao/p/11836916.html
Copyright © 2020-2023  润新知