说说资源加载的问题(Class的getResource方法)

背景

最近的项目中又碰到了这个问题。需要在web应用的指定路径去手工加载一个文件。

熟悉的双亲委派原则

先来看一张图,对于java的开发来说,熟悉的不能再熟悉了。

说说资源加载的问题(Class的getResource方法)_第1张图片

  • Bootstrap ClassLoader:加载诸如rt.jar等核心文件

  • Extension ClassLoader:加载ext目录下的扩展文件

  • System ClassLoader:习惯性也称之为AppClassLoader。加载用户的classpath下的文件。

  • User-Defined ClassLoader:用户自定义类加载器。

类加载器寻找的文件路径

上一节我们知道了各个类加载器所负责的功能,可是我们应该知道的更细节一些,每个类加载器到底是去哪些目录查找文件的呢?

BootstrapClassLoader

BootstapClassLoader要找的路径如下,代码见sun.misc.Launcher.BootClassPathHolder:

说说资源加载的问题(Class的getResource方法)_第2张图片

再往上翻,可以看到bootClassPath的来源:

0?wx_fmt=png

我们可以清楚的看到,BootstrapClassLoader是从System.getProperty("sun.boot.class.path")来获取查找路径的。在我本机,获得的URL列表如下:

D:\programme\jdk\jdk8U74\jre\lib\resources.jar;D:\programme\jdk\jdk8U74\jre\lib\rt.jar;D:\programme\jdk\jdk8U74\jre\lib\sunrsasign.jar;D:\programme\jdk\jdk8U74\jre\lib\jsse.jar;D:\programme\jdk\jdk8U74\jre\lib\jce.jar;D:\programme\jdk\jdk8U74\jre\lib\charsets.jar;D:\programme\jdk\jdk8U74\jre\lib\jfr.jar;D:\programme\jdk\jdk8U74\jre\classes

ExtClassLoader

获取ExtClassLoader装载的目录如下, 代码见sun.misc.Launcher.ExtClassLoader:

说说资源加载的问题(Class的getResource方法)_第3张图片

我们可以清楚的看到,ExtClassLoader是从System.getProperty("java.ext.dirs")来获取查找路径的。在我本机,获得的URL列表如下:

D:\programme\jdk\jdk8U74\jre\lib\ext;C:\windows\Sun\Java\lib\ext 

AppClassLoader

能看到直接调用new AppClassLoader()的地方就一处, 代码见sun.misc.Launcher.AppClassLoader:

说说资源加载的问题(Class的getResource方法)_第4张图片

很清楚的看到, AppClassLoader是直接获取了当前系统的java.class.path属性。在我的本机上,获得的URL列表如下:

D:\programme\jdk\jdk8U74\jre\lib\charsets.jar;D:\programme\jdk\jdk8U74\jre\lib\deploy.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\access-bridge-64.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\cldrdata.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\dnsns.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\jaccess.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\jfxrt.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\localedata.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\nashorn.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\sunec.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\sunjce_provider.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\sunmscapi.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\sunpkcs11.jar;D:\programme\jdk\jdk8U74\jre\lib\ext\zipfs.jar;D:\programme\jdk\jdk8U74\jre\lib\javaws.jar;D:\programme\jdk\jdk8U74\jre\lib\jce.jar;D:\programme\jdk\jdk8U74\jre\lib\jfr.jar;D:\programme\jdk\jdk8U74\jre\lib\jfxswt.jar;D:\programme\jdk\jdk8U74\jre\lib\jsse.jar;D:\programme\jdk\jdk8U74\jre\lib\management-agent.jar;D:\programme\jdk\jdk8U74\jre\lib\plugin.jar;D:\programme\jdk\jdk8U74\jre\lib\resources.jar;D:\programme\jdk\jdk8U74\jre\lib\rt.jar;D:\alibaba\content-alibaba\tinker\target\classes;D:\programme\opensource\maven\repository\commons-digester\commons-digester\1.7\commons-digester-1.7.jar;
...maven...D:\programme\opensource\maven\repository\commons-beanutils\commons-beanutils\1.6\commons-beanutils-1.6.jar;D:\programme\opensource\maven\repository\commons-logging\commons-logging\1.0\commons-logging-1.0.jar; 
D:\programme\IntelliJ IDEA Community Edition 2016.1.1\lib\idea_rt.jar

我们可以关注到以下细节:

  1. 路径列表中包含了ext扩展jar目录。

  2. 应用目录的class目录被包含D:\alibaba\content-alibaba\tinker\target\classes;

  3. maven目录被包含

  4. idea的一个文件idea_rt.jar被包含(这是因为idea启动java程序时默认会在-classpath中带上这个jar)

文件查找过程之getClass().getClassLoader().getResource("data/resource.xml")

getClass().getClassLoader():一般来说我们在应用里边使用的话获得的ClassLoader都是AppClassLoader或者用户自定义ClassLoader。

getResource()这是关键的方法。

说说资源加载的问题(Class的getResource方法)_第5张图片

熟悉的同学明白,这依然是一个双亲委派的资源寻找过程, 假如一个文件在classpath下,基本的寻找过程如下:

接下来看具体寻找文件的过程sun.misc.URLClassPath:

说说资源加载的问题(Class的getResource方法)_第6张图片

两个关键点:

  • getNextLoader 主要获取当前URL查找项对应的Loader。大部分情况下,每个URL都可以归类为FileLoader和JarLoader。分别代表当前查找URL是一个文件夹或是一个jar包。

说说资源加载的问题(Class的getResource方法)_第7张图片

  • loader.getResource

先来看看jar包是如何找文件的,见sun.misc.URLClassPath.JarLoader:

说说资源加载的问题(Class的getResource方法)_第8张图片

核心就是使用JarEntry.getJarEntry方法。该方法可以在jar中查找对应文件是否存在。举个栗子:

/**
 * 2017/2/14 11:13 by 热海
 */public class JarFileTest {

    public static void main(String[] args) throws Exception{
        JarFile jar = new JarFile("D:\\programme\\opensource\\maven\\repository\\com\\alibaba\\citrus\\citrus-webx-all-in-one\\3.0.6\\citrus-webx-all-in-one-3.0.6.jar");
        JarEntry entry = jar.getJarEntry("META-INF/services-data-resolver-factories.bean-definition-parsers");
        System.out.print(entry);
    }
}

以上代码就是判断META-INF/services-data-resolver-factories.bean-definition-parsers这个路径下的文件在citrus-webx-all-in-one-3.0.6.jar中是否存在。

再来看看对于文件夹FileLoader, 看看是如何寻找文件的,见sun.misc.URLClassPath.FileLoader:

说说资源加载的问题(Class的getResource方法)_第9张图片

能看到是文件路径拼接。也就是当前查找的URL路径 + 资源文件路径的拼接。

文件查找过程之getClass().getClassLoader().getResource("/data/resource.xml")

上一节我们看了getClassLoader().getResource("data/resource.xml"), 这一节我们看下

我们现在已经知道,有两种Loader,一种专门负责从jar中加载资源(JarLoader),一种负责从文件夹中加载资源(FileLoader)。先来看看FileLoader:

说说资源加载的问题(Class的getResource方法)_第10张图片

由于原始的参数是/data/resource.xml, 是个绝对的路径,所以和baseUrl进行组装是失败,获得的url依然是/data/resource.xml。此时继续代码走入了异常流,因为代码强制要求最终的url要以baseUrl开头。所以此路不通。

再看JarLoader:

说说资源加载的问题(Class的getResource方法)_第11张图片

正常情况下就是没问题的:

说说资源加载的问题(Class的getResource方法)_第12张图片

所以绝对路径查找在此方法中行不通。

文件查找过程之getClass().getResource("data/resource.xml")

我们再变一下,现在去掉getClassLoader(), 直接getResource获取文件看看是什么结果。

直接获取resource的时候,相比之前,多了一个很重要的方法,见java.lang.Class:

说说资源加载的问题(Class的getResource方法)_第13张图片

我们可以看到,之后也是使用当前类的类加载器去寻找资源,这样就和上边调用getClassLoader没什么两样了!所以关键点在于,这个resolveName方法做了什么事情,继续见java.lang.Class:

说说资源加载的问题(Class的getResource方法)_第14张图片

方法很简单。当碰到"/"不是第一个字符的资源是,直接包装路径为当前类的同目录下资源。假设当前类是

文件查找过程之getClass().getResource("/data/resource.xml")

继续。

可以看到当查找的资源首字符是"/"时。直接是取后边的部分然后再调用getResource。所以这个小标题的代码可以写成getClass().getClassLoader().getResource("data/resource.xml"), 等于说又是一个在classpath中寻找相对资源路径的问题。

meta-index文件,空间换时间

想象这样一个场景。我想要在bootClassLoader中进行查找一个tmp.log的文件是否存在这些jar包中。

如果每次都来一个进行查询的话。不是不可以,但是感觉总是很累(我本地的JDK版本1.8.0_91,rt.jar已经超过60M了)。并且我们的JDK是一个固定的版本在哪里,JDK中的jar也不会发生变化。

有没有办法呢?来看meta-index文件。在我的本机,位置在:file:///D:/programme/jdk/jdk8U74/jre/lib/meta-index, 文件内容如下:

% VERSION 2% WARNING: this file is auto-generated; do not edit
% UNSUPPORTED: this file and its format may change and/or
%   may be removed in a future release# charsets.jarsun/nio
sun/awt# jce.jarjavax/crypto
sun/security
META-INF/ORACLE_J.RSA
META-INF/ORACLE_J.SF# jfr.jaroracle/jrockit/jdk/jfr
com/oracle/jrockit/
! jsse.jar
sun/security
com/sun/net/
! management-agent.jar
@ resources.jar
com/sun/java/util/jar/pack/META-INF/services/sun.util.spi.XmlPropertiesProvider
META-INF/services/javax.print.PrintServiceLookup
com/sun/corba/
META-INF/services/javax.sound.midi.spi.SoundbankReader
sun/print
META-INF/services/javax.sound.midi.spi.MidiFileReader
META-INF/services/sun.java2d.cmm.CMMServiceProvide
javax/swing
META-INF/services/javax.sound.sampled.spi.AudioFileReader
META-INF/services/javax.sound.midi.spi.MidiDeviceProvider
sun/net
META-INF/services/javax.sound.sampled.spi.AudioFileWriter
com/sun/imageio/
META-INF/services/sun.java2d.pipe.RenderingEngine
META-INF/mimetypes.default
META-INF/services/javax.sound.midi.spi.MidiFileWriter
sun/rmi
javax/sql
META-INF/services/com.sun.tools.internal.ws.wscompile.Plugin
com/sun/rowset/
META-INF/services/javax.print.StreamPrintServiceFactory
META-INF/mailcap.default
java/lang
sun/text
javax/xml
META-INF/services/javax.sound.sampled.spi.MixerProvider
com/sun/xml/
META-INF/services/com.sun.tools.internal.xjc.Plugin
com/sun/java/swing/com/sun/jndi/
com/sun/org/
META-INF/services/javax.sound.sampled.spi.FormatConversionProvider
! rt.jar
com/sun/java/util/jar/pack/java/
org/ietf/com/sun/beans/
com/sun/tracing/
com/sun/java/browser/com/sun/corba/
com/sun/media/
com/sun/awt/
com/sun/management/
sun/
com/sun/jmx
com/sun/demo/
com/sun/imageio/
com/sun/net/
com/sun/rmi/
org/w3c/com/sun/swing/
com/sun/activation/
com/sun/nio/
com/sun/rowset/
org/jcp/com/sun/istack/
jdk/
com/sun/naming/
org/xml/org/omg/com/sun/security/
com/sun/image/
com/sun/xml/
com/sun/java/swing/com/oracle/com/sun/java_cup/
com/sun/jndi/
com/sun/accessibility/
com/sun/org/
javax/  

简单来说, 这个文件告诉BootClassLoader, 针对某个jar包,哪些目录下是纯class文件。 同时也透出了当前jar包的目录情况。这样在判断一个普通文件是否在JDK包时,只需和meta-index比对就好了,会极大的节省时间。具体文件分析过程见sun.misc.MetaIndex。

综述

BB了这么多,简单的总结一下。

  1. getResource方法不支持操作系统绝对路径。资源的寻找都是基于某个绝对的地址开始相对的查找。

  2. 资源的查找也是一个双亲委派的查找过程

  3. BootstrapClassLoader的查找路径来自于系统的java.class.path属性。

  4. ExtClassLoader的查找路径来自于系统的java.ext.dirs属性。

  5. AppClassLoader的查找路径来自于系统的java.class.path属性。

  6. BootstrapClassLoader的meta-index的方案值得学习。

  7. 可以利用JarFile和JarEntry来获取某个文件是否在jar包中。注意,参数路径要是相对的。

  8. getClass().getClassLoader().getResource("data/resource.xml")从classpath遍历

  9. getClass().getClassLoader().getResource("/data/resource.xml")此路不通

  10. getClass().getResource("data/resource.xml")是以当前类为基准做相对路径查找

  11. getClass().getResource("/data/resource.xml")功能等同于第八条。

如果有理解不对的地方,欢迎各位指正。

本文转自同事,作者: 热海

你可能感兴趣的:(说说资源加载的问题(Class的getResource方法))