为什么Class.getResource("...")返回null
getResource("...")和getResourceAsStream("...")
Class#getResource("...")
首先Class#getResource("...")实际上也是通过ClassLoader来加载资源的
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class. 比如Runtime.class等由BootstrapClassLoader进行加载的这些Class
return ClassLoader.getSystemResource(name);
}
// 通过加载此类的类加载器来加载资源
return cl.getResource(name);
}
同理,getResourceAsStream也是一样,就不再赘述了
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);
}
Class#getResource("...")时如果传入绝对路径,则去掉开头的/,如果是相对路径,则拼上使用到的Class对应的包前缀
/**
* Add a package name prefix if the name is not absolute Remove leading "/"
* if name is absolute
*/
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;
}
ClassLoader#getResource("...")
下面来看ClassLoader加载资源的逻辑,同类加载的逻辑也是一样
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;
}
向上委托的每一级类加载器如果父类加载器找不到目标资源,则通过自己的findResource尝试搜索资源

findResource方法需要子类进行实现,可以直接看URLClassLoader的实现逻辑,因为其他的子类都继承自URLClassLoader
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;
}
对于BootstrapClassLoader来说,同样也是通过对应的URLClassPath来查找资源的
/**
* Find resources from the VM's built-in classloader.
*/
private static URL getBootstrapResource(String name) {
URLClassPath ucp = getBootstrapClassPath();
Resource res = ucp.getResource(name);
return res != null ? res.getURL() : null;
}
URLClassPath
URLClassPath是基于URL来表示类路径下的资源信息
ArrayList<Loader> loaders;
HashMap<String, Loader> lmap;
Loader有2种,一种是基于文件的FileLoader,另外一种是基于jar包的JarLoader
我们可以通过如下方式获取每个URLClassPath内部保存了哪些URL,搜索资源时就是从这些URL下进行搜索
URLClassPath bootstrapClassPath = ...; // 拿到ClassLoader的URLClassPath
Arrays.stream(extClassLoader.getURLs()).forEach(System.out::println);
对于对于BootstrapClassLoader,通过Launcher.getBootstrapClassPath();获取URLClassPath
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/resources.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/rt.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jsse.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jce.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/charsets.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jfr.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/classes
ExtClassLoader内部的URL列表如下所示
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/access-bridge-64.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/cldrdata.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/dnsns.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/jaccess.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/jfxrt.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/localedata.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/nashorn.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunec.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunjce_provider.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunmscapi.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunpkcs11.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/zipfs.jar
AppClassLoader
URLClassLoader extClassLoader = (URLClassLoader) Test.class.getClassLoader();
Arrays.stream(extClassLoader.getURLs()).forEach(System.out::println);
// 结果包含ExtClassLoader和BootstrapClassLoader的URL列表
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/charsets.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/deploy.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/access-bridge-64.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/cldrdata.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/dnsns.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/jaccess.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/jfxrt.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/localedata.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/nashorn.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunec.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunjce_provider.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunmscapi.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/sunpkcs11.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/ext/zipfs.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/javaws.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jce.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jfr.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jfxswt.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/jsse.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/management-agent.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/plugin.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/resources.jar
file:/D:/Develop/Java/JDK/oraclejdk1.8.0_361/jre/lib/rt.jar
file:/D:/Develop/Code/classloader-demo/target/classes/
Java9+
由于Java SE 9在命名模块中的类上调用getResourceXXX只会定位该模块中的资源,因此它不会像以前版本那样搜索类路径。因此,当您使用Class.getResourceAsStream()时,它将尝试在包含java.lang.Class的模块(即java.base模块)中定位资源。一般情况下的资源不在该模块中,因此它返回null。必须让java 9+搜索你模块中的文件,这很可能是一个“未命名的模块”。您可以通过将Class更改为模块中定义的任何类来实现这一点,以便使java使用正确的类加载器。
@CallerSensitive
public URL getResource(String name) {
name = resolveName(name);
Module thisModule = getModule();
if (thisModule.isNamed()) {
// 命名模块
// check if resource can be located by caller
if (Resources.canEncapsulate(name)
&& !isOpenToCaller(name, Reflection.getCallerClass())) {
return null;
}
// resource not encapsulated or in package open to caller
String mn = thisModule.getName();
ClassLoader cl = getClassLoader0();
try {
if (cl == null) {
// 从该模块下加载资源
return BootLoader.findResource(mn, name);
} else {
return cl.findResource(mn, name);
}
} catch (IOException ioe) {
return null;
}
}
// unnamed module 匿名模块
ClassLoader cl = getClassLoader0();
if (cl == null) {
return ClassLoader.getSystemResource(name);
} else {
return cl.getResource(name);
}
}
总结
从上面的分析来看,假设要查找的资源路径是 /html/index.html
- 直接使用某个Class.getResource或者Class.getResourceAsStream来获取资源,使用的相对路径是Classpath根路径 + 该Class的包名 + /html/index.html
- 使用ClassLoader来获取资源使用的相对路径是相对路径是Classpath根路径 + /html/index.html
- 如果是JDK9+版本,并且存在模块化的情况下,还要注意模块下是否包含要加载的资源
从以上三个方面来排查就能知道为什么加载资源为null了

浙公网安备 33010602011771号