java 中资源文件的加载方式

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13

├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── test
│ │ ├── ResourceTest.java
│ │ └── Resource.java
│ └── resources
│ ├── conf
│ │ └── config.json
│ └── application.properties
└── pom.xml

读取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ResourceTest {

public static void main(String[] args) {
// 1、通过Class的getResource方法
String a1 = ResourceTest.class.getResource("/com/test/Resource.class").getPath();
String a2 = ResourceTest.class.getResource("Resource.class").getPath();
String a3 = ResourceTest.class.getResource("/application.properties").getPath();
String a4 = ResourceTest.class.getResource("../../application.properties").getPath();
String a5 = ResourceTest.class.getResource("/conf/config.json").getPath();
String a6 = ResourceTest.class.getResource("../../conf/config.json").getPath();

// 2、通过本类的ClassLoader的getResource方法
String b1 = ResourceTest.class.getClassLoader().getResource("com/test/Resource.class").getPath();
String b2 = ResourceTest.class.getClassLoader().getResource("application.properties").getPath();
String b3 = ResourceTest.class.getClassLoader().getResource("conf/config.json").getPath();

// 3、通过ClassLoader的getSystemResource方法
String c1 = ClassLoader.getSystemClassLoader().getResource("com/test/Resource.class").getPath();
String c2 = ClassLoader.getSystemClassLoader().getResource("application.properties").getPath();
String c3 = ClassLoader.getSystemClassLoader().getResource("conf/config.json").getPath();

// 4、通过ClassLoader的getSystemResource方法
String d1 = ClassLoader.getSystemResource("com/test/Resource.class").getPath();
String d2 = ClassLoader.getSystemResource("application.properties").getPath();
String d3 = ClassLoader.getSystemResource("conf/config.json").getPath();

// 5、通过Thread方式
String e1 = Thread.currentThread().getContextClassLoader().getResource("com/test/Resource.class").getPath();
String e2 = Thread.currentThread().getContextClassLoader().getResource("application.properties").getPath();
String e3 = Thread.currentThread().getContextClassLoader().getResource("conf/config.json").getPath();
}
}

总结规律如下:

  • Class.getResource()的资源获取如果以 / 开头, 则从根路径开始搜索资源
  • Class.getResource()的资源获取如果不以 / 开头, 则从当前类所在的路径开始搜索资源
  • ClassLoader.getResource()的资源获取不能以 / 开头, 统一从根路径开始搜索资源

源码分析

首先看一下Class类中的两个方法实现

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

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);
}

可以看到, Class类先通过resoveName方法解析出资源文件路径, 然后委托ClassLoader去加载资源的, 首先看一下resolveName方法的实现

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.test.ResourceTest -> com/test/, 最后拼接上资源路径入参作为资源文件的路径

再看一下ClassLoader的两个方法实现

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

public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}

getResourceAsStream方法也是先调用同类的getResource方法, 所以重点看一下getResource方法的实现. 该方法首先使用了双亲委派机制来加载资源, 最终的父类ClassLoader使用getBootstrapClassPath去加载资源, 如果父类没加载到还会findResource去加载, 因此ClassLoader子类可以通过覆盖findResource方法来实现自定义的资源加载实现.

本示例中就是通过AppClassLoaderfindResource方法(实际是继承自URLClassLoader)加载到的资源, 该方法在URLClassLoader中实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public URL findResource(final String name) {
/*
* The same restriction to finding classes applies to resources
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
return ucp.findResource(name, true);
}
}, acc);

return url != null ? ucp.checkURL(url) : null;
}