在 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进阶的文章,感谢关注