最近的项目中又碰到了这个问题。需要在web应用的指定路径去手工加载一个文件。
先来看一张图,对于java的开发来说,熟悉的不能再熟悉了。
Bootstrap ClassLoader:加载诸如rt.jar等核心文件
Extension ClassLoader:加载ext目录下的扩展文件
System ClassLoader:习惯性也称之为AppClassLoader。加载用户的classpath下的文件。
User-Defined ClassLoader:用户自定义类加载器。
上一节我们知道了各个类加载器所负责的功能,可是我们应该知道的更细节一些,每个类加载器到底是去哪些目录查找文件的呢?
BootstapClassLoader要找的路径如下,代码见sun.misc.Launcher.BootClassPathHolder
:
再往上翻,可以看到bootClassPath的来源:
我们可以清楚的看到,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装载的目录如下, 代码见sun.misc.Launcher.ExtClassLoader
:
我们可以清楚的看到,ExtClassLoader是从System.getProperty("java.ext.dirs")
来获取查找路径的。在我本机,获得的URL列表如下:
D:\programme\jdk\jdk8U74\jre\lib\ext;C:\windows\Sun\Java\lib\ext
能看到直接调用new AppClassLoader()的地方就一处, 代码见sun.misc.Launcher.AppClassLoader
:
很清楚的看到, 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
我们可以关注到以下细节:
路径列表中包含了ext扩展jar目录。
应用目录的class目录被包含D:\alibaba\content-alibaba\tinker\target\classes;
maven目录被包含
idea的一个文件idea_rt.jar被包含(这是因为idea启动java程序时默认会在-classpath中带上这个jar)
getClass().getClassLoader()
:一般来说我们在应用里边使用的话获得的ClassLoader都是AppClassLoader或者用户自定义ClassLoader。
getResource()这是关键的方法。
熟悉的同学明白,这依然是一个双亲委派的资源寻找过程, 假如一个文件在classpath下,基本的寻找过程如下:
接下来看具体寻找文件的过程sun.misc.URLClassPath
:
两个关键点:
getNextLoader 主要获取当前URL查找项对应的Loader。大部分情况下,每个URL都可以归类为FileLoader和JarLoader。分别代表当前查找URL是一个文件夹或是一个jar包。
loader.getResource
先来看看jar包是如何找文件的,见sun.misc.URLClassPath.JarLoader
:
核心就是使用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
:
能看到是文件路径拼接。也就是当前查找的URL路径 + 资源文件路径的拼接。
上一节我们看了getClassLoader().getResource("data/resource.xml"), 这一节我们看下
我们现在已经知道,有两种Loader,一种专门负责从jar中加载资源(JarLoader),一种负责从文件夹中加载资源(FileLoader)。先来看看FileLoader:
由于原始的参数是/data/resource.xml, 是个绝对的路径,所以和baseUrl进行组装是失败,获得的url依然是/data/resource.xml。此时继续代码走入了异常流,因为代码强制要求最终的url要以baseUrl开头。所以此路不通。
再看JarLoader:
正常情况下就是没问题的:
所以绝对路径查找在此方法中行不通。
我们再变一下,现在去掉getClassLoader(), 直接getResource获取文件看看是什么结果。
直接获取resource的时候,相比之前,多了一个很重要的方法,见java.lang.Class:
我们可以看到,之后也是使用当前类的类加载器去寻找资源,这样就和上边调用getClassLoader没什么两样了!所以关键点在于,这个resolveName方法做了什么事情,继续见java.lang.Class:
方法很简单。当碰到"/"不是第一个字符的资源是,直接包装路径为当前类的同目录下资源。假设当前类是
继续。
可以看到当查找的资源首字符是"/"时。直接是取后边的部分然后再调用getResource。所以这个小标题的代码可以写成getClass().getClassLoader().getResource("data/resource.xml"), 等于说又是一个在classpath中寻找相对资源路径的问题。
想象这样一个场景。我想要在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了这么多,简单的总结一下。
getResource方法不支持操作系统绝对路径。资源的寻找都是基于某个绝对的地址开始相对的查找。
资源的查找也是一个双亲委派的查找过程
BootstrapClassLoader的查找路径来自于系统的java.class.path属性。
ExtClassLoader的查找路径来自于系统的java.ext.dirs属性。
AppClassLoader的查找路径来自于系统的java.class.path属性。
BootstrapClassLoader的meta-index的方案值得学习。
可以利用JarFile和JarEntry来获取某个文件是否在jar包中。注意,参数路径要是相对的。
getClass().getClassLoader().getResource("data/resource.xml")从classpath遍历
getClass().getClassLoader().getResource("/data/resource.xml")此路不通
getClass().getResource("data/resource.xml")是以当前类为基准做相对路径查找
getClass().getResource("/data/resource.xml")功能等同于第八条。
如果有理解不对的地方,欢迎各位指正。
本文转自同事,作者: 热海