关于ClassLoader中getResource与getResourceAsStream的疑问
编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。
背景
某日临近下班,同事希望在任意类中获取项目的绝对路径,且不通过 Request 方式获取,但始终无法得到预想的结果。起初推测可能是 System.getProperty("java.class.path") 的问题,但经验证,该方法仅打印出 CLASSPATH 环境变量的值,并非项目绝对路径。
随后经过检索,找到了 Class 类的 getResource 与 getResourceAsStream 两个方法。这两个方法底层会委托给 ClassLoader 对应的同名方法。本以为这样可以解决问题,但在验证过程中却发现了奇怪的现象。
软件环境:Windows XP、Resin、Tomcat 6.0、MyEclipse、JDK 1.5
验证过程
验证思路如下:
- 定义一个
Servlet,在该Servlet中调用Path类的getPath方法。getPath方法返回工程 classpath 的绝对路径,并显示在 JSP 中。 - 在
Path类中,通过Class.getResourceAsStream读取当前工程 classpath 路径中的a.txt文件,并将其写入到getResource路径下的b.txt文件中。
由于时间匆忙,代码组织较为简单,但足以体现上述两个功能。
代码实现
Path.java
public class Path {
public String getPath() throws IOException {
InputStream is = this.getClass().getResourceAsStream("/a.txt");
File file = new File(Path.class.getResource("/").getPath() + "/b.txt");
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = is.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
is.close();
return this.getClass().getResource("/").getPath();
}
}PathServlet.java
public class PathServlet extends HttpServlet {
private static final long serialVersionUID = 4443655831011903288L;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Path path = new Path();
request.setAttribute("path", path.getPath());
PrintWriter out = response.getWriter();
out.println("Class.getResource('/').getPath():" + path.getPath());
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}问题现象
在此之前,使用 main 函数测试 Path.class.getResource("/").getPath() 打印出的预想路径为:/D:/work/project/EhCacheTestAnnotation/WebRoot/WEB-INF/classes/
然而,将 WEB 应用部署到 Resin 下运行定义好的 Servlet 时,出乎意料的结果是:/D:/work/resin-3.0.23/webapps/WEB-INF/classes/
特别奇怪的是,路径中丢掉了项目名称 EhCacheTestAnnotation。
还有一点值得注意,getPath 方法中使用 getResourceAsStream("/a.txt") 却正常读到了位于下图的 a.txt 文件。

然后代码将内容写到了如下图的 b.txt 中。代码实现逻辑为:File file = new File(Path.class.getResource("/").getPath()+"/b.txt"); 本意是想在 a.txt 文件目录下再写入 b.txt,结果却和料想的不一样。

请注意,区别依然是丢掉了项目名称。
分析与对比
稍微总结一下:程序中使用 ClassLoader 的两个方法:getResourceAsStream 和 getResource。事实证明在 WEB 应用的场景下,二者得到了不同的结果。
虽然这两个方法名字不同,功能自然不同,但 getResourceAsStream 理应获取指定路径下的文件。示例中参数为 "/a.txt",它正确读取了 /D:/work/resin-3.0.23/webapps/EhCacheTestAnnotation/WEB-INF/classes/ 下的 a.txt。可是,将文件写到 getResource 方法的 getPath 返回路径的 b.txt 文件时,路径却缺少了项目名称 EhCacheTestAnnotation。
暂且得出一个结论:通过 getResourceAsStream 和 getResource 两个方法获取的路径表现是不同的。 但是为什么呢?
于是查看了 ClassLoader 的源码,贴出 getResource 和 getResourceAsStream 的源码如下:
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;
}
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}从代码中看,getResourceAsStream 将获取 URL 的任务委托给了 getResource 方法。令人困惑的是,既然委托关系存在,为何表现不一致?由此彻底迷茫,百思不得其解。
但是没有因此放弃,继续回想了一遍整个过程:
- 在
main函数中,测试getResource与getResourceAsStream是完全相同的,结果正确。 - 将其部署到 Resin 下,导致了
getResource与getResourceAsStream获取的路径不一致。
一个闪光点是:是不是与 Web 容器有关? 于是换成 Tomcat 6.0。令人惊讶的是,“奇迹”出现了,换成 Tomcat 结果就一致了,与预想的一致。
在 Tomcat 下运行结果如下图:

对,这就是我想要的结果。
因此我对 Resin 产生了疑虑,之前也因为在 Resin 下程序报错、在 Tomcat 下正常运行而纠结了好久。记得看《松本行弘的程序世界》中对 C++ 中的多继承是这样评价的(大概意思):多重继承带来的负面影响多数是由于使用不当造成的。是不是因为对 Resin 使用不得当才使得和 Tomcat 下得到不同的结果?
原因定位
最终,在查阅 Resin 配置文件 resin.conf 时,在 <host-default> 标签下发现了这样一段配置:
<class-loader>
<compiling-loader path="webapps/WEB-INF/classes"/>
<library-loader path="webapps/WEB-INF/lib"/>
</class-loader>其中的 compiling-loader 很可能与之有关。遂将其注释掉,一切正常。担心是错觉,于是将 compiling-loader 的 path 属性改成:webapps/WEB-INF/classes1,然后运行 PathServlet,b.txt 位置如下图:

确实与 compiling-loader 有关。
解决方案
终于通过将 <class-loader> 标签注释掉,同样可以在 Resin 中获取“预想”的路径。验证了的确是使用 Resin 的配置出了问题。
遗留疑问
但是没有这样就结束,继续对 getResource 的源码进行了跟进,由于能力有限,没有弄清楚 getResource 的原理。最终留下了两个疑问:
- 如果追踪到
getResource方法的最底层(也许是 JVM 层面),它实现的原理是什么? - 为何 Resin 中
<class-loader>的配置会对getResource产生影响,但是对getResourceAsStream毫无影响(getResourceAsStream可是将获取路径委托给getResource的啊)?还是这里理解或者使用错误了?
在这里也请明白人指明。
说明
本文基于 JDK 1.5、Resin 3.0.23 及 Tomcat 6.0 环境验证。现代 JDK 版本及 Web 容器(如 Tomcat 8/9/10, Spring Boot 内嵌容器等)在类加载机制及路径获取方式上可能已有显著变化,请以当前官方文档为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。