Java NIO无法从JRT镜像中读取文件。

8
当我们通过jlink创建Java运行时,它会将所有的Java类/资源放置在JRT映像文件lib/modules中。
这是我使用的基本Maven项目资源结构:
src
  main
    resources
      dict
        xkcd_en

我只是想读取xkcd_en文本文件。如果我们查看JRT文件,它在这里:

>> jimage list /path/to/lib/modules
...
Module: main
    dict/xkcd_en
...

此外,我已经在module-info中明确打开了它,以防万一:

module main {
    opens dict;
    // ..rest code omitted
}

我只能将文件作为输入流来读取:

可行:

public static InputStream getResourceAsStream(String resource) {
    return FileUtils.class.getResourceAsStream(resource);
}

System.out.println(new BufferedReader(
    new InputStreamReader(getResourceAsStream("/dict/xkcd_en")))
            .lines().collect(Collectors.joining("\n"))
);

无法正常工作:

但如果我尝试使用Java NIO API获取文件URI并读取它,它无法正常工作:

public static URL getResourceOrThrow(String resource) {
    URL url = FileUtils.class.getResource(resource);
    Objects.requireNonNull(url);
    return url;
}

1 - Java NIO找不到文件。但是它绝对存在,否则getResource()会返回null

System.out.println(Paths.get(getResourceOrThrow("/dict/xkcd_en").toURI()));
// /main/dict/xkcd_en

Files.readAllLines(Paths.get(getResourceOrThrow("/dict/xkcd_en").toURI()));

Caused by: java.nio.file.NoSuchFileException: /main/dict/xkcd_en
        at java.base/jdk.internal.jrtfs.JrtFileSystem.checkNode(JrtFileSystem.java:494)
        at java.base/jdk.internal.jrtfs.JrtFileSystem.getFileContent(JrtFileSystem.java:253)
        at java.base/jdk.internal.jrtfs.JrtFileSystem.newInputStream(JrtFileSystem.java:342)
        at java.base/jdk.internal.jrtfs.JrtPath.newInputStream(JrtPath.java:631)
        at java.base/jdk.internal.jrtfs.JrtFileSystemProvider.newInputStream(JrtFileSystemProvider.java:322)

2 - 如果您直接使用FileSystem,则情况相同:

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
System.out.println(fs.getPath("main/dict/xkcd_en"));
// main/dict/xkcd_en

Files.readAllLines(fs.getPath("main/dict/xkcd_en")));

Caused by: java.nio.file.NoSuchFileException: /main/dict/xkcd_en
    at java.base/jdk.internal.jrtfs.JrtFileSystem.checkNode(JrtFileSystem.java:494)

3 - Java NIO甚至不知道jrt:/协议是什么。

Files.readAllLines(Paths.get(getResourceOrThrow("/dict/xkcd_en").toExternalForm()));

Caused by: java.nio.file.InvalidPathException: Illegal char <:> at index 3: jrt:/main/dict/xkcd_en
    at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
    at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:229)
    at java.base/java.nio.file.Path.of(Path.java:147)
    at java.base/java.nio.file.Paths.get(Paths.java:69)

这里是 JRT FS 的 规范

jrt URL 是一个分层的 URI,符合 RFC 3986,其语法如下:

jrt:/[$MODULE[/$PATH]]

其中 $MODULE 是可选的模块名称,$PATH(如果存在)是该模块内特定类或资源文件的路径。jrt URL 的含义取决于其结构:

  • jrt:/$MODULE/$PATH 引用给定 $MODULE 中名为 $PATH 的特定类或资源文件。
  • jrt:/$MODULE 引用 $MODULE 模块中的所有类和资源文件。
  • jrt:/ 引用存储在当前运行时镜像中的所有类和资源文件的整个集合。

所以,我认为获得的路径看起来没问题。我错在哪里了吗?


5
jrt文件系统提供程序存在一个错误,请参见:https://bugs.openjdk.java.net/browse/JDK-8216553。您可以通过在文件路径前加上“/modules”来解决此问题。同时,请注意,您的示例使用了toExternalForm调用Paths.get(String),它是用于将路径字符串转换为平台文件系统上的文件,而不是jrtfs的文件。 - Alan Bateman
@AlanBateman 我以为已经有人报告过这个问题了。我应该先检查一下错误数据库。毕竟,一年多以前我就在这个答案中添加了一个解决方法,所以我想我对这个 bug 的了解时间更长。 - Holger
@AlanBateman 这个 bug 是仅仅涉及到从 URI 转换为 Path 的问题,还是 modules 目录本来就应该对 jrtfs 的用户透明的? - Slaw
4
问题出在将URL转换为jrt路径上。"modules"目录对于jrtfs的用户非常重要。 - Alan Bateman
1个回答

8

JRT 文件系统

您引用的 JEP 的一部分特别涉及 URL。如果你再往下读一点,你会发现它讨论了 JRT 文件系统:

A built-in NIO FileSystem provider for the jrt URL scheme ensures that development tools can enumerate and read the class and resource files in a run-time image by loading the FileSystem named by the URL jrt:/, as follows:

FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
byte[] jlo = Files.readAllBytes(fs.getPath("modules", "java.base",
                                          "java/lang/Object.class"));

The top-level modules directory [emphasis added] in this filesystem contains one subdirectory for each module in the image. The top-level packages directory [emphasis added] contains one subdirectory for each package in the image, and that subdirectory contains a symbolic link to the subdirectory for the module that defines that package.

正如您所看到的,JRT文件系统在根目录下直接有两个目录:modulespackages。这些是作为JDK-8066492的一部分添加的,它们的目的由该问题描述。因此,问题不在于NIO API无法读取JRT映像中的资源,而在于:

/main/dict/xkcd_en

确实不存在。该资源实际上位于以下位置:

/modules/main/dict/xkcd_en

JRT URL

JRT URL共有三种形式(在你提问中引用了JEP部分中都有提到):

  1. jrt:/$MODULE/$PATH
  2. jrt:/$MODULE
  3. jrt:/

第一种形式是用于访问JRT镜像中特定资源的,也是我们关心的。正如您所看到的,URL不包括上面提到的顶级目录。可以将URL视为始终相对于modules目录。


JRT文件系统提供程序漏洞

话虽如此,正如Alan Bateman在Stack Overflow上指出的,您遇到了一个漏洞。当您拥有JRT URL并尝试将其转换为Path时,您应该得到一个指向现有文件的Path。问题是,这种转换没有考虑modules目录。

Java 13通过JDK-8224946修复了此漏洞。


感谢您的详细解释。作为API用户,我认为我不需要关心文件系统内部数据的存储方式。如果URL成功解析,则必须可以使用它来访问相应的资源。 - Evan
1
根据Alan Bateman在他的评论中提到的问题,看起来至少部分行为是一个bug。 - Slaw

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