在 Java 中, 为了从相对路径读取文件, 经常会使用的方法便是:
- xxx.class.getResource();
- xxx.class.getClassLoader().getResource();
在 Spring 中, 我们还可以通过 Spring 提供的 Resource 进行一些操作:
- ClassPathResource
- FileSystemResource
- ServletContextResource
- Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
这里简单总结下他们的区别:
ClassLoader##getResource()
这个方法是今天的主角.
我们都知道 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()
- 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 # ClassPathResource()
在 Spring 中, 对 Resource 进行了扩展, 使得 Resource 能够适应更多的应用场景,
不过 ClssPathResource() 底层依然是 ClassLoader##getResource(), 因此 ClassLoader##getResource()d 的特性, ClassPathResource 也支持.
- 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 以 / 和不以 / 开头没有区别
Spring # ServletContextResource
ServletContextResource 是针对 Servlet 来做的, 我们知道, Servlet 规定 webapp 目录如下:
而 ServletContextResource 的路径则是 xxx 目录下为起点. 也就是可以通过 ServletContextResource 获取到 form.html 等资源.
同时对比上面的 ClassPathResource 我们可以发现:
"classpath:com"
等价于:
- ServletContextResource("WEB-INF/classes/com")
- Spring # FileSystemResource
FileSystemResource 没什么好说的, 就是系统目录资源, 比如
- ApplicationContext ctx =
- new FileSystemXmlApplicationContext("D://test.xml");
它的标记头为 file:
例如:
- ApplicationContext ctx =
- new FileSystemXmlApplicationContext("flie:D://test.xml");
来源: http://www.bubuko.com/infodetail-3283141.html