在 Java 中,为了从相对路径读取文件,经常会使用的方法便是:
xxx.class.getResource(); xxx.class.getClassLoader().getResource(); 复制代码
在 Spring 中,我们还可以通过 Spring 提供的 Resource 进行一些操作:
ClassPathResource
FileSystemResource
ServletContextResource
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
复制代码
这里简单总结下他们的区别:
这个方法是今天的主角。
我们都知道 ClassLoader 的作用是用来加载 .class 文件的,并且 ClassLoader 是遵循 Java 类加载中的双亲委派机制的。
那么, ClassLoader 是如何找到这个 .class 文件的呢?答案是 URLClassPath
Java 中自带了3个 ClassLoader 分别是 BootStrap ClassLoader , EtxClassLoader , AppClassLoader ,
这3个 ClassLoader 都继承自 URLClassLoader ,而 URLClassLoader 中包含一个 URLClassPath 用来记录每个 ClassLoader 对应的加载 .class 文件的路径,当需要加载资源的时候,只管从 URLClassPath 对应的路径查找即可。
下面是测试代码:
System.out.println("BootStrap ClassLoader ");
Stream.of(System.getProperty("sun.boot.class.path").split(";")).forEach(System.out::println);
System.out.println("ExtClassLoader:");
Stream.of(System.getProperty("java.ext.dirs").split(";")).forEach(System.out::println);
System.out.println("AppClassLoader:");
Stream.of(System.getProperty("java.class.path").split(";")).forEach(System.out::println);
复制代码
输出如下:
BootStrap ClassLoader H:/java/jdk1.8/jre/lib/resources.jar H:/java/jdk1.8/jre/lib/rt.jar H:/java/jdk1.8/jre/lib/sunrsasign.jar H:/java/jdk1.8/jre/lib/jsse.jar H:/java/jdk1.8/jre/lib/jce.jar H:/java/jdk1.8/jre/lib/charsets.jar H:/java/jdk1.8/jre/lib/jfr.jar H:/java/jdk1.8/jre/classes ExtClassLoader: H:/java/jdk1.8/jre/lib/ext C:/Windows/Sun/Java/lib/ext AppClassLoader: H:/java/jdk1.8/jre/lib/charsets.jar H:/java/jdk1.8/jre/lib/deploy.jar H:/java/jdk1.8/jre/lib/ext/access-bridge-64.jar H:/java/jdk1.8/jre/lib/ext/cldrdata.jar H:/java/jdk1.8/jre/lib/ext/dnsns.jar H:/java/jdk1.8/jre/lib/ext/jaccess.jar H:/java/jdk1.8/jre/lib/ext/jfxrt.jar H:/java/jdk1.8/jre/lib/ext/localedata.jar H:/java/jdk1.8/jre/lib/ext/nashorn.jar H:/java/jdk1.8/jre/lib/ext/sunec.jar H:/java/jdk1.8/jre/lib/ext/sunjce_provider.jar H:/java/jdk1.8/jre/lib/ext/sunmscapi.jar H:/java/jdk1.8/jre/lib/ext/sunpkcs11.jar H:/java/jdk1.8/jre/lib/ext/zipfs.jar H:/java/jdk1.8/jre/lib/javaws.jar H:/java/jdk1.8/jre/lib/jce.jar H:/java/jdk1.8/jre/lib/jfr.jar H:/java/jdk1.8/jre/lib/jfxswt.jar H:/java/jdk1.8/jre/lib/jsse.jar H:/java/jdk1.8/jre/lib/management-agent.jar H:/java/jdk1.8/jre/lib/plugin.jar H:/java/jdk1.8/jre/lib/resources.jar H:/java/jdk1.8/jre/lib/rt.jar F:/spring-test/target/classes 复制代码
AppClassLoader 负责常用的 JDK jar 以及项目所依赖的 jar 包
上述参数可以通过 sun.misc.Launcher.class获得
通过输出的参数,我们可以清晰的看出来各个 ClassLoader 负责的区域
说了这么多,这个和 ClassLoader#getResource() 有什么关系呢?
关系很大,前面刚刚提问过, ClassLoader 是如何读取 .class 文件的呢?
答案是 URLClassPath#getResource() 方法:每个 UrlClassLoader 都是通过 URLClassPath 来存储对应的加载区域,当需要查找 .class 文件的时候,就通过 URLClassPath#getResource() 查找即可。
下面再来看看 ClassLoader#getResource()
//双亲委派查找
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;
}
//由于BootStrap ClassLoader是C++写的,Java拿不到其引用。
//因此这里单独写了一个方法获取BootStrapResource()
private static URL getBootstrapResource(String name) {
URLClassPath ucp = getBootstrapClassPath();
Resource res = ucp.getResource(name);
return res != null ? res.getURL() : null;
}
复制代码
URLClassLoader#findResource() public URL findResource(final String name) {
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
return ucp.findResource(name, true);
}
}, acc);
return url != null ? ucp.checkURL(url) : null;
}
复制代码
我们只用注意这一句 ucp.findResource(name, true); ,这边是查找 .class 文件的方法,因此我们可以总结出通过 ClassLoader#getResource() 的流程:
AppClassLoader 委派给 ExtClassLoader 查找是否存在对应的资源 ExtClassLoader 委派给 BootStrap ClassLoader 查找是有存在对应的资源 BootStrap ClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回 BootStrap ClassLoader 未查找到对应资源, ExtClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回 ExtClassLoader 未查找到对应资源, AppClassLoader 通过 URLClasspath 查找自己加载的区域,查找到了即返回 AppClassLoader 未查找到,抛出异常。 这个过程,就和加载 .class 文件的过程一样。
在这里我们就可以发现,通过 ClassLoader#getResource() 可以获取 JDK 资源,所依赖的 JAR 包资源等
因此,我们甚至可以这样写:
//读取 java.lang.String.class 的字节码
InputStream inputStream =Test.class.getClassLoader().getResourceAsStream("java/lang/String.class");
try(BufferedInputStream bufferedInputStream=new BufferedInputStream(inputStream)){
byte[] bytes=new byte[1024];
while (bufferedInputStream.read(bytes)>0){
System.out.println(new String(bytes, StandardCharsets.UTF_8));
}
}
复制代码
明白了 ClassLoader#getResource() ,其实本篇文章就差不多了,因为后面要将的几个方法,底层都是 ClassLoader#getResource()
class##getResource() 底层就是 ClassLoader#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);
}
复制代码
不过有个小区别就在于 class#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;
}
复制代码
这个 resolveName() 大致就是判断路径是相对路径还是绝对路径,如果是相对路径,则资源名会被加上当前项目的根路径:
Test.class.getResource("spring-config.xml");
复制代码
resolve之后变成
com/dengchengchao/test/spring-config.xml 复制代码
这样的资源就只能在当前项目中找到。
Test.class.getResource("test.txt"); //相对路径
Test.class.getResource("/"); //根路径
复制代码
注意: ClassLoader#getResource() 不能以 / 开头
在 Spring 中,对 Resource 进行了扩展,使得 Resource 能够适应更多的应用场景,
protected URL resolveURL() {
if (this.clazz != null) {
return this.clazz.getResource(this.path);
} else {
return this.classLoader != null ? this.classLoader.getResource(this.path) : ClassLoader.getSystemResource(this.path);
}
}
复制代码
ClassPathResource 用于读取 classes 目录文件
一般来说,对于 SpringBoot 项目,打包后的项目结构如下:
xxx.jar
|--- BOOT-INF
|--------|--classes
|--------|----|--com
|--------|----|-- application.properties
|--------|----|--logback.xml
| -------|-- lib
|--- META-INF
|--- org
可以看到, ClassPathResource() 的起始路径便是 classes ,平时我们读取的 application.properties 便是使用 ClasspathResource() 获取的
在平时使用的过程中,有三点需要注意:
classpath 和 classpath* 区别:
classpath:只会返回第一个查找到的文件 classpath*:会返回所有查找到的文件
在 Spring 中,需要直接表示使用 ClassPathResource() 来查找的话,可以直接添加 classpath: 头
使用 classpath 以 / 和不以 / 开头没有区别
ServletContextResource 是针对 Servlet 来做的,我们知道, Servlet 规定 webapp 目录如下:
而 ServletContextResource 的路径则是 xxx 目录下为起点。也就是可以通过 ServletContextResource 获取到 form.html 等资源。
同时对比上面的 ClassPathResource 我们可以发现:
"classpath:com" 复制代码
等价于:
ServletContextResource("WEB-INF/classes/com")
复制代码
FileSystemResource 没什么好说的,就是系统目录资源,比如
ApplicationContext ctx =
new FileSystemXmlApplicationContext("D://test.xml");
复制代码
它的标记头为 file:
例如:
ApplicationContext ctx =
new FileSystemXmlApplicationContext("flie:D://test.xml");
复制代码
如果觉得写得不错,欢迎关注微信公众号:逸游Java ,每天不定时发布一些有关Java进阶的文章,感谢关注