最近在看spring的资源获取时发现JDK里存在几种不同方式的资源获取,因比较混乱特地总结起来帮助和我一样混乱的人理解。下面是我项目的类结构图,在 src/main/java
下有两个类 ResourceTest.java和Resource.java
,resources
目录下有两个资源文件 request.xml 和 conf/sysConf.json
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com
│ │ │ │ ├── alipay
│ │ │ │ │ ├── ResourceTest.java
│ │ │ │ │ └── Resource.java
│ │ └── resources
│ │ │ ├── conf
│ │ │ │ ├── sysConf.json │ │ │ └── request.xml └── local.iml
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ ├── com
│ │ │ │ ├── alipay
│ │ │ │ │ ├── ResourceTest.java
│ │ │ │ │ └── Resource.java
│ │ └── resources
│ │ │ ├── conf
│ │ │ │ ├── sysConf.json │ │ │ └── request.xml └── local.iml
在ResourceTest中,我想获取Resource这个类以及request.xml、sysConf这两个资源文件,可以分为Class和ClassLoader两种方式来获取资源,而ClassLoader则又可以细分为3种方式:
public class ResourceTest { public static void main(String[] args) { // 1、通过Class的getResource方法 String a1 = ResourceTest.class.getResource("/com/alipay/Resource.class").getPath(); String a2 = ResourceTest.class.getResource("Resource.class").getPath(); String a3 = ResourceTest.class.getResource("/request.xml").getPath(); String a4 = ResourceTest.class.getResource("../../request.xml").getPath(); String a5 = ResourceTest.class.getResource("/conf/sysConf.json").getPath(); String a6 = ResourceTest.class.getResource("../../conf/sysConf.json").getPath(); // 2、通过本类的ClassLoader的getResource方法 String b1 = ResourceTest.class.getClassLoader().getResource("com/alipay/Resource.class").getPath(); String b2 = ResourceTest.class.getClassLoader().getResource("request.xml").getPath(); String b3 = ResourceTest.class.getClassLoader().getResource("conf/sysConf.json").getPath(); // 3、通过ClassLoader的getSystemResource方法 String c1 = ClassLoader.getSystemClassLoader().getResource("com/alipay/Resource.class").getPath(); String c2 = ClassLoader.getSystemClassLoader().getResource("request.xml").getPath(); String c3 = ClassLoader.getSystemClassLoader().getResource("conf/sysConf.json").getPath(); // 4、通过ClassLoader的getSystemResource方法 String d1 = ClassLoader.getSystemResource("com/alipay/Resource.class").getPath(); String d2 = ClassLoader.getSystemResource("request.xml").getPath(); String d3 = ClassLoader.getSystemResource("conf/sysConf.json").getPath(); // 5、通过Thread方式 String e1 = Thread.currentThread().getContextClassLoader().getResource("com/alipay/Resource.class").getPath(); String e2 = Thread.currentThread().getContextClassLoader().getResource("request.xml").getPath(); String e3 = Thread.currentThread().getContextClassLoader().getResource("conf/sysConf.json").getPath(); } }
public class ResourceTest { public static void main(String[] args) { // 1、通过Class的getResource方法 String a1 = ResourceTest.class.getResource("/com/alipay/Resource.class").getPath(); String a2 = ResourceTest.class.getResource("Resource.class").getPath(); String a3 = ResourceTest.class.getResource("/request.xml").getPath(); String a4 = ResourceTest.class.getResource("../../request.xml").getPath(); String a5 = ResourceTest.class.getResource("/conf/sysConf.json").getPath(); String a6 = ResourceTest.class.getResource("../../conf/sysConf.json").getPath(); // 2、通过本类的ClassLoader的getResource方法 String b1 = ResourceTest.class.getClassLoader().getResource("com/alipay/Resource.class").getPath(); String b2 = ResourceTest.class.getClassLoader().getResource("request.xml").getPath(); String b3 = ResourceTest.class.getClassLoader().getResource("conf/sysConf.json").getPath(); // 3、通过ClassLoader的getSystemResource方法 String c1 = ClassLoader.getSystemClassLoader().getResource("com/alipay/Resource.class").getPath(); String c2 = ClassLoader.getSystemClassLoader().getResource("request.xml").getPath(); String c3 = ClassLoader.getSystemClassLoader().getResource("conf/sysConf.json").getPath(); // 4、通过ClassLoader的getSystemResource方法 String d1 = ClassLoader.getSystemResource("com/alipay/Resource.class").getPath(); String d2 = ClassLoader.getSystemResource("request.xml").getPath(); String d3 = ClassLoader.getSystemResource("conf/sysConf.json").getPath(); // 5、通过Thread方式 String e1 = Thread.currentThread().getContextClassLoader().getResource("com/alipay/Resource.class").getPath(); String e2 = Thread.currentThread().getContextClassLoader().getResource("request.xml").getPath(); String e3 = Thread.currentThread().getContextClassLoader().getResource("conf/sysConf.json").getPath(); } }
以上所有的方式都能够获取到对应的资源文件。
由于maven打包会把 src/main/java
和 src/main/resources
下的文件放到 target/classes
下,所以下面统一以根路径代表此目录,总结起来有以下几个规律:
- Class.getResource()的资源获取如果以
/
开头,则从根路径开始搜索资源。 - Class.getResource()的资源获取如果不以
/
开头,则从当前类所在的路径开始搜索资源。 - ClassLoader.getResource()的资源获取不能以
/
开头,统一从根路径开始搜索资源。
下面还是老习惯,翻开源码看看为什么是这样的规律。
Class.getResource()
public java.net.URL getResource(String name) { name = resolveName(name); // 获得类的类加载器,默认为AppClassLoader ClassLoader cl = getClassLoader0(); if (cl==null) { // A system class. return ClassLoader.getSystemResource(name); } return cl.getResource(name); }
public java.net.URL getResource(String name) { name = resolveName(name); // 获得类的类加载器,默认为AppClassLoader ClassLoader cl = getClassLoader0(); if (cl==null) { // A system class. return ClassLoader.getSystemResource(name); } return cl.getResource(name); }
可以很清晰的看出上面的资源获取流程:
- 解析文件路径,变成ClassLoader所支持的路径。
- 获取该类的类加载器,默认为AppClassLoader,接着调用它的getResource方法。
- 如果类加载器获取失败,直接走ClassLoader的getSystemResource方法来获取
我们看看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(); // 截取当前类所在的包和name使用 / 进行拼接 int index = baseName.lastIndexOf('.'); if (index != -1) { name = baseName.substring(0, index).replace('.', '/') +"/"+name; } } else { // 如果以 / 开头,截取 / 后面的内容 name = name.substring(1); } return name; }
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(); // 截取当前类所在的包和name使用 / 进行拼接 int index = baseName.lastIndexOf('.'); if (index != -1) { name = baseName.substring(0, index).replace('.', '/') +"/"+name; } } else { // 如果以 / 开头,截取 / 后面的内容 name = name.substring(1); } return name; }
原理和我们上面分析的一样。因为Class的getResource最终还是调用的ClassLoader,所以我们接着来看ClassLoader的相关资源获取方法。
ClassLoader.getResource()
public URL getResource(String name) { URL url; if (parent != null) { // 递归调用 url = parent.getResource(name); } else { // 使用BootstrapClassLoader发现资源 url = getBootstrapResource(name); } if (url == null) { // 真正去找对应的url url = findResource(name); } return url; }
public URL getResource(String name) { URL url; if (parent != null) { // 递归调用 url = parent.getResource(name); } else { // 使用BootstrapClassLoader发现资源 url = getBootstrapResource(name); } if (url == null) { // 真正去找对应的url url = findResource(name); } return url; }
这个方法比较有意思的地方在于它使用了双亲委派机制来加载资源(回顾双亲委派机制 点我 ),它从BootstrapClassLoader一层层往下找直到最后找到该资源。本例中是通过AppClassLoader来找到了对应的资源(实际使用了URLClassLoader的findResource方法)
ClassLoader.getSystemResource()
这种方式对应于例子中的方式四,相比于方式三就多了一个空判断。
public static URL getSystemResource(String name) { // systemClassLoader就是从Launcher获取的AppClassLoader ClassLoader system = getSystemClassLoader(); if (system == null) { return getBootstrapResource(name); } return system.getResource(name); }
public static URL getSystemResource(String name) { // systemClassLoader就是从Launcher获取的AppClassLoader ClassLoader system = getSystemClassLoader(); if (system == null) { return getBootstrapResource(name); } return system.getResource(name); }
getSystemResource和getResource的区别就在于你是否实现了自己的类加载器,如果都是使用的默认的AppClassLoader,这两个方法的作用一样。
线程上下文加载方式
这种加载方式对应于例子中的最后一种方式,它是使用 java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是 AppClassLoader
,具体设置参考 sun.misc.Launcher
的构造函数。
更多关于线程上下文加载的疑问可以参考之前我的一篇文章:理解TCCL:线程上下文类加载器
因为线程上下文加载方式的灵活性