引言
我们经常会看到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@18b4aac2
file:/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@18b4aac2
file:/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。这里就不做过多解释了。