引言
我们经常会看到ClassLoader和Class的getResource方法,它们两个存在相互调用关系,并且路径问题也是一个难点,今天我们来看一下这两个方法的联系和区别。
ClassLoader的getResource方法
方法定义
/*** Finds the resource with the given name. A resource is some data* (images, audio, text, etc) that can be accessed by class code in a way* that is independent of the location of the code.** <p> The name of a resource is a '<tt>/</tt>'-separated path name that* identifies the resource.** <p> This method will first search the parent class loader for the* resource; if the parent is <tt>null</tt> the path of the class loader* built-in to the virtual machine is searched. That failing, this method* will invoke {@link #findResource(String)} to find the resource. </p>** @apiNote When overriding this method it is recommended that an* implementation ensures that any delegation is consistent with the {@link* #getResources(java.lang.String) getResources(String)} method.** @param name* The resource name** @return A <tt>URL</tt> object for reading the resource, or* <tt>null</tt> if the resource could not be found or the invoker* doesn't have adequate privileges to get the resource.** @since 1.1*/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;}
这个方法根据指定的name来查找资源,name是一个以/分隔的路径名,这个路径用来标识这个资源。首先,它会判断当前的ClassLoader是否有parent,如果有,就让parent来加载这个资源,如果没有parent,就会让jvm内置的类加载器来加载这个资源,如果还是没找到,就会调用findResource方法来获取。我们来看findResource方法:
protected URL findResource(String name) {return null;}
这个方法返回null,看来需要ClassLoader自己去实现。
路径问题
首先,如果是相对路径的话,就是相对于classpath的路径。在maven工程打包完成后,classpath就是classes文件夹这个位置。
我们来看一个例子,加入一个当前的工程结构如下:

在resources下面有一个文件TestClassLoader.txt,我们打包完成后,它就会出现在classes这个文件夹下(与application.properties一样),那么我们怎么在TestGetResource这个类中使用getResource方法拿到这个资源呢?很简单:
public class TestGetResource {public static void main(String[] args) {ClassLoader classLoader = ClassLoader.getSystemClassLoader();System.out.println(classLoader);URL resource = classLoader.getResource("TestClassLoader.txt");System.out.println(resource);}}
只需要通过相对路径来,参数直接就是TestClassLoader.txt,因为打包之后的这个文件在classes文件夹下,就是classpath下,所以直接根据相对路径来就行,输出:
sun.misc.Launcher$AppClassLoader@18b4aac2file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/TestClassLoader.txt
那如果这种情况呢:
TestClassLoader.txt在templates文件夹下,获取也很简单,还是通过相对路径,直接加上templates这一层即可:
public class TestGetResource {public static void main(String[] args) {ClassLoader classLoader = ClassLoader.getSystemClassLoader();System.out.println(classLoader);URL resource = classLoader.getResource("templates/TestClassLoader.txt");System.out.println(resource);}}
输出:
我们可以这样来看classLoader的getResource方法的路径是相对哪个路径的,也就是根路径:
public static void main(String[] args) {ClassLoader classLoader = ClassLoader.getSystemClassLoader();System.out.println(classLoader);URL rootResource = classLoader.getResource("");System.out.println(rootResource);}
输出:
sun.misc.Launcher$AppClassLoader@18b4aac2file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/
Class的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);}
首先,它调用了resolveName,这个方法很重要,它修改了name的值,然后调用的是classLoader的getResource方法,根据之前对getResource方法的理解,resolveName这个方法要做的应该是把name改为ClassLoader的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;}
首先,如果name以/开头,就会去掉/,保留/后面的路径,这样就成为了一个相对路径,而ClassLoader中相对路径就是相对于classpath的路径,所以,如果用class的getResource方法来获取上面的TestClassLoader.txt,可以这样写:
public class TestGetResource {public static void main(String[] args) {URL resource = TestGetResource.class.getResource("/templates/TestClassLoader.txt");System.out.println(resource);}}
它传给ClassLoader的getResource方法的name就是去掉/之后的template/TestClassLoader.txt,输出:
file:/Users/cuihualong/develop/code/SkyWalking/consumer/target/classes/templates/TestClassLoader.txt
这是Class的getResource的name以/开头的情况。
如果不是以/开头,也就是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;}}
它是获取了当前类的完整包路径。例如我们这样写:
public static void main(String[] args) {URL resource = TestGetResource.class.getResource("templates/TestClassLoader.txt");System.out.println(resource);}
注意,这样是取不到资源的,只是为了展示一下路径不包含/时的逻辑,我们在resolveName方法上打上断点,来看一些name的值:
可以看到,它会在你给到的name前面加上class所代表的类也就是TestGetResource所在的包路径。也就是说,当你name是不带/也就是相对路径时,resolveName会认为你的这个相对路径是相对于class所代表的的类的相对路径,它要把它转化为相对与classpath的路径,所以就会加上包路径。所以这样写的话,我们就找不到资源了。
从上面resolveName的两种处理方式我们可以看出,经过这个方法处理后的路径总是不带/的,也就是相对路径,并且是相对classpath的路径,这样就能传给ClassLoader的getResource方法了。
知道了resolveName方法的处理规则,我们就能轻松地写出正确的路径了。
getResourceAsStream方法
ClassLoader类还有一个方法我们也经常用到,它与getResource方法关系密切,就是getResourceAsStream方法,它的实现很简单:
public InputStream getResourceAsStream(String name) {URL url = getResource(name);try {return url != null ? url.openStream() : null;} catch (IOException e) {return null;}}
首先调用了getResource方法来获取URL,然后直接打开这个URL。这里就不做过多解释了。
