JVM获取资源的途径和差别

JVM加载配置资源文件有两种方式:

1、ClassLoader#getResource

2、Class#getResource

两者之间的区别:

ClassLoader并不关心当前类的包名路径,它永远以classpath为基点来定位资源。需要注意的是在用ClassLoader加载资源时,路径不要以”/“开头,所有以”/“开头的路径都返回null

Class.getResource如果资源名是绝对路径(以”/“开头),那么会以classpath为基准路径去加载资源,如果不以”/“开头,那么以这个类的Class文件所在的路径为基准路径去加载资源

从源代码层次分析一下,这个结论对不对?

ClassLoader#getResource

1
2
3
4
5
6
7
8
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
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;
}

相对路径

1
2
3
4
5
6
//classloader从根节点开始查找
final URL resource = Thread.currentThread().getContextClassLoader().getResource("root.properties");
System.err.println(resource);
Assertions.assertNotNull(resource);

//成功获取

这样子是正常获取到资源

绝对路径

1
2
3
4
5
//以目录作对比,这样写,应该也没问题,但为什么返回是null呢?
//并且很多资料都直接说 classloader加载资源时,不要以 / 开头,以 / 开头都会返回null
final URL resource1 = Thread.currentThread().getContextClassLoader().getResource("/root.properties");
System.err.println(resource1);
Assertions.assertNull(resource1);

很多资料的结论classloader加载资源时,不要以 / 开头,以 / 开头都会返回null,是正确的。

从debug中可以看出来为什么以/开头,获取不到对应的资源。

主要还是对根节点的理解不一样:

classcloader以根节点去查找,是以当前的classpath为起点;

而以 / 开头,就变成类似root下了,自然查找不到

相对路径

1
2
final URL resource2 = Thread.currentThread().getContextClassLoader().getResource("./root.properties");
System.err.println(resource2);

这样是可以成功查找。

Class#getResouce

1
2
3
4
5
6
7
8
9
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}

看着Class#getResource,多加了一步resolveName,其实还是使用了Classloader#getResource方法

其中resolveName()

name不以’/‘开头时,默认是从此类所在的包下取资源;

name以’/‘开头时,则会substring(1),踢掉/,绝对路径变相对数据再从ClassPath根下获取资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

在与com.zhuxingsheng.lang.GetResourceTest同目录下面创建package.properties

直接写文件名

1
2
3
4
5

final URL resource = GetResourceTest.class.getResource("package.properties");
System.err.println(resource);

//获取资源正常

根据debug信息,可以看出会从当前类目录去查找

绝对路径

1
2
3
4
final URL resource1 = getClass().getResource("/com/zhuxingsheng/lang/package.properties");
System.err.println(resource1);

//获取资源正常

从resolveName方法,可知,写了绝对路径,会被substring(1),也就是手动拼接完事包路径,走了resolveName的第一个不以 / 开头的分支路径。

也就是classloader#getResource不要写绝对路径。

完整相对路径

1
2
3
4
5

final URL resource1 = getClass().getResource("com/zhuxingsheng/lang/package.properties");
System.err.println(resource1);

//获取失败

从debug中可以看出,就是把完整的路径拼接了两次,路径变成了com/zhuxingsheng/lang/com/zhuxingsheng/lang/package.properties

结论

经过源代码的debug,上文的结论是正确的。

ClassLoader并不关心当前类的包名路径,它永远以classpath为基点来定位资源。需要注意的是在用ClassLoader加载资源时,路径不要以”/“开头,所有以”/“开头的路径都返回null

Class.getResource如果资源名是绝对路径(以”/“开头),那么会以classpath为基准路径去加载资源,如果不以”/“开头,那么以这个类的Class文件所在的路径为基准路径去加载资源

但在springboot中,自定义了classloader,打破了上述规则。下篇再看springboot的加载机制。

公众号:码农戏码
欢迎关注微信公众号『码农戏码』