有没有一种方法可以判断类路径资源是文件还是目录?

11
例如,假设某个JAR包(例如Guava)中存在com.google包,那么以下代码片段在stream.read()行会抛出NullPointerException异常。
ClassLoader classLoader = getClass().getClassLoader();
URL resource = classLoader.getResource("com/google");
InputStream stream = resource.openStream();
System.out.println(stream.toString()); // Fine -- stream is not null
stream.read(); // NPE inside FilterInputStream.read()!

如果将com/google与文件系统中而不是JAR中的软件包交换,则代码片段根本不会崩溃。实际上,它似乎会读取该目录中由换行符分隔的文件,尽管我无法想象该行为在任何地方都有规定。

是否有一种方法可以测试资源路径“com/google”指向“常规”资源文件还是目录?

如果将com/google替换为文件系统中的软件包而不是JAR,则代码片段完全不会崩溃。事实上,它似乎会读取该目录中由换行符分隔的文件,尽管我无法想象这种行为在任何地方得到了规定。

是否有一种方法可以测试资源路径“com/google”指向“普通”资源文件还是目录?


try { stream.read(); System.out.println("It's on the filesystem!"); } catch (NullPointerException npe) { System.out.println("It's in a JAR!"); } - Adrian Petrescu
@AdrianPetrescu 我想区分类路径目录和文件,而不是“JAR文件中的文件”和“文件系统上的文件或目录”。另外,我更倾向于不依赖于任何未记录的内容。 - Tavian Barnes
3个回答

10
由于协议处理程序在加载这些资源时涉及到一些未指定的行为,因此这有点混乱。在这种特定情况下,有两个处理程序:sun.net.www.protocol.file.Handlersun.net.www.protocol.jar.Handler,它们各自以稍微不同的方式处理目录情况。根据一些实验,以下是它们各自的做法:

sun.net.www.protocol.file.Handler:

  • What this Handler does is open a FileURLConnection, which does exactly what you discovered it did when confronted with a directory. You can check if it's a directory just with:

    if (resource.getProtocol().equals("file")) {
        return new File(resource.getPath()).isDirectory();
    }
    

sun.net.www.protocol.jar.Handler:

  • 然而,这个Handler打开了一个JarURLConnection,最终到达一个ZipCoder。如果你看一下那段代码,你会发现有趣的事情:由于JAR zip文件实际上不包含名为com/google的文件,所以从本地JNI调用中返回null到包装它的流。

然而,有解决方法。虽然ZipCoder找不到com/google,但它找到com/google/(由于某种原因,大多数ZIP接口都是这样工作的)。在这种情况下,jzentry将被找到,并且它将返回一个空字节。

因此,如果你想判断一个资源是否为目录,可以通过尝试在资源路径后加上斜杠/这是URLClassLoader期望的目录格式)来排除所有随机的实现特性。如果ClassLoader.getResource()返回非空,则表示它是一个目录。如果返回空,则尝试不带斜杠的路径。如果返回非空,则表示它是一个文件。如果仍然返回空,则表示该资源不存在。

这种方法可能有些hacky,但我认为没有更好的办法了。希望这能帮到你!


2
太好了,谢谢。遗憾的是,即使file.txt是一个普通文件,getResource("path/to/file.txt/")仍会返回非空值。 - Tavian Barnes
哇,Go Java。好的,这只发生在“file”协议上,还是在“jar”协议上也会发生?我怀疑只有前者。 - Adrian Petrescu
如果只发生在“file”上,正如我所怀疑的那样,那么只需使用上面的resource.getProtocol()检查来单独处理该情况,而对于“jar”,尾随/技巧仍然有效。 - Adrian Petrescu

1

没有安全和通用的方法来检测这个。当您使用ClassLoader.getResource()时,ClassLoader可以在URL中返回实际上任何东西,原则上甚至可以返回您从未见过的内容,如果ClassLoader实现了自己的URL方案(和协议)。

您唯一的选择是分析getResource()返回的URL,协议应该提示它是什么(例如“file://”)。但请注意,根据环境,它可能会返回您没有计划的内容。

但是,要访问资源,您不关心它来自哪里(如果您正在调试配置问题,则可能会关心,但您的代码不应关心)。

一般情况下,您不应该对返回的InputStream的功能做出假设,即不要依赖于它支持标记/重置等操作。唯一安全的操作就是简单地读取Stream。如果在读取期间发生IOException,则表示访问资源存在问题(网络连接丢失等)。

编辑:我认为getResource()应该只返回资源(例如文件或zip文件条目),而永远不应该返回目录(因为它们不是资源)。但是,我不能保证每个可能的ClassLoader都会这样做,也不确定正确的行为是什么(如果它甚至在某个地方被指定了的话)。


我想答案可能是这样的。我确实像这样处理类路径资源,但如果有人无意中将“com/company”而不是“com/company/resource.txt”传递给我的API,我希望做一些更智能的事情,而不是在稍后的read()中抛出NPE。 - Tavian Barnes
如果URL返回了一些具有延迟错误检测的流,就像你的片段所示,我会在第一次读取时就接受它失败。我对此唯一挑剔的是,它确实应该抛出一个IOException而不是NPE。但我肯定不会为了JRE中的怪异而费尽心思。垃圾进,垃圾出。 - Durandal
遗憾的是,在这种情况下输入的是垃圾数据,我们花了很长时间远程调试一个奇怪的堆栈跟踪。 - Tavian Barnes

-3

我认为有两种解决方案。

  1. 基于路径本身的分析的朴素解决方案。如果以 .jar.zip.war.ear 结尾,则是文件。否则就是目录。我认为这种方法在99.99%的情况下都有效,除非有人故意让你失败。例如,通过定义看起来像目录但实际上是文件或反之的软链接。
  2. 尝试模仿JVM逻辑,相对于当前工作目录解释类路径的路径。因此,使用 new File(".") 检索当前工作目录,然后获取类路径,拆分它,并针对每个元素使用 new File(".", classPathElement),除非它使用绝对路径定义。

祝你好运。


1
也许我可以更清楚地表达我的问题。我说的不是类路径条目,而是类路径资源,即那些JAR/WAR文件中的文件。例如,在Maven中,它是src/main/resources。 - Tavian Barnes
@Tavian Barnes,你的问题很清楚。这就是我所说的。一旦你能够读取类路径条目,你肯定可以确定类路径资源的类型。如果条目是文件路径,只需使用常规文件API来检查资源的类型。如果它是一个jar包,使用JarInputStream - AlexR
哦,好的,我没有理解你的意思是要扫描类路径并自己打开条目。虽然这是合理的,但我希望能够使用现有的ClassLoader基础结构处理任意类路径。否则,我的应用程序将在异域环境中出现故障。 - Tavian Barnes
资源不等于文件。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接