为什么Class.getResource("...")返回null

getResource("...")和getResourceAsStream("...")

https://stackoverflow.com/questions/26328040/intellij-idea-getclass-getresource-return-null/65173942#65173942

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尝试搜索资源

image
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

  1. 直接使用某个Class.getResource或者Class.getResourceAsStream来获取资源,使用的相对路径是Classpath根路径 + 该Class的包名 + /html/index.html
  2. 使用ClassLoader来获取资源使用的相对路径是相对路径是Classpath根路径 + /html/index.html
  3. 如果是JDK9+版本,并且存在模块化的情况下,还要注意模块下是否包含要加载的资源

从以上三个方面来排查就能知道为什么加载资源为null了

posted @ 2025-07-05 12:08  vonlinee  阅读(41)  评论(0)    收藏  举报