编者注:本文为历史博文归档;涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效,阅读时请注意时效性。

背景

某日临近下班,同事希望在任意类中获取项目的绝对路径,且不通过 Request 方式获取,但始终无法得到预想的结果。起初推测可能是 System.getProperty("java.class.path") 的问题,但经验证,该方法仅打印出 CLASSPATH 环境变量的值,并非项目绝对路径。

随后经过检索,找到了 Class 类的 getResourcegetResourceAsStream 两个方法。这两个方法底层会委托给 ClassLoader 对应的同名方法。本以为这样可以解决问题,但在验证过程中却发现了奇怪的现象。

软件环境:Windows XP、Resin、Tomcat 6.0、MyEclipse、JDK 1.5

验证过程

验证思路如下:

  1. 定义一个 Servlet,在该 Servlet 中调用 Path 类的 getPath 方法。getPath 方法返回工程 classpath 的绝对路径,并显示在 JSP 中。
  2. 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 的两个方法:getResourceAsStreamgetResource。事实证明在 WEB 应用的场景下,二者得到了不同的结果。

虽然这两个方法名字不同,功能自然不同,但 getResourceAsStream 理应获取指定路径下的文件。示例中参数为 "/a.txt",它正确读取了 /D:/work/resin-3.0.23/webapps/EhCacheTestAnnotation/WEB-INF/classes/ 下的 a.txt。可是,将文件写到 getResource 方法的 getPath 返回路径的 b.txt 文件时,路径却缺少了项目名称 EhCacheTestAnnotation

暂且得出一个结论:通过 getResourceAsStreamgetResource 两个方法获取的路径表现是不同的。 但是为什么呢?

于是查看了 ClassLoader 的源码,贴出 getResourcegetResourceAsStream 的源码如下:

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 方法。令人困惑的是,既然委托关系存在,为何表现不一致?由此彻底迷茫,百思不得其解。

但是没有因此放弃,继续回想了一遍整个过程:

  1. main 函数中,测试 getResourcegetResourceAsStream 是完全相同的,结果正确。
  2. 将其部署到 Resin 下,导致了 getResourcegetResourceAsStream 获取的路径不一致。

一个闪光点是:是不是与 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-loaderpath 属性改成:webapps/WEB-INF/classes1,然后运行 PathServletb.txt 位置如下图:

确实与 compiling-loader 有关。

解决方案

终于通过将 <class-loader> 标签注释掉,同样可以在 Resin 中获取“预想”的路径。验证了的确是使用 Resin 的配置出了问题。

遗留疑问

但是没有这样就结束,继续对 getResource 的源码进行了跟进,由于能力有限,没有弄清楚 getResource 的原理。最终留下了两个疑问:

  1. 如果追踪到 getResource 方法的最底层(也许是 JVM 层面),它实现的原理是什么?
  2. 为何 Resin 中 <class-loader> 的配置会对 getResource 产生影响,但是对 getResourceAsStream 毫无影响(getResourceAsStream 可是将获取路径委托给 getResource 的啊)?还是这里理解或者使用错误了?

在这里也请明白人指明。

说明

本文基于 JDK 1.5Resin 3.0.23Tomcat 6.0 环境验证。现代 JDK 版本及 Web 容器(如 Tomcat 8/9/10, Spring Boot 内嵌容器等)在类加载机制及路径获取方式上可能已有显著变化,请以当前官方文档为准。