【问题标题】:File is read on Windows but not on a Linux container?在 Windows 上读取文件但在 Linux 容器上不读取?
【发布时间】:2020-08-06 20:15:42
【问题描述】:

正如标题所说,在 linux 容器上运行相同代码时,我无法读取文件(csv 文件)的内容

private Set<VehicleConfiguration> loadConfigurations(Path file, CodeType codeType) throws IOException {

    log.debug("File exists? " + Files.exists(file));
    log.debug("Path " + file.toString());
    log.debug("File " + file.toFile().toString());
    log.debug("File absolute path " + file.toAbsolutePath().toString());

    String line;
    Set<VehicleConfiguration> configurations = new HashSet<>(); // this way we ignore duplicates in the same file
    try(BufferedReader br = new BufferedReader(new FileReader(file.toFile()))){
        while ((line = br.readLine()) != null)   {
            configurations.add(build(line, codeType));
        }
    }


    log.debug("Loaded " + configurations.size() + " configurations");
    return configurations;
}

日志返回“true”和两个系统中的文件路径(本地在 windows 和 linux docker 容器上)。在 Windows 上它加载“15185 个配置”,但在容器上它加载“0 个配置”。

该文件存在于linux上,我使用bash并自己检查。我使用head命令,文件有行。

在此之前,我尝试使用 Files.lines,如下所示:

var vehicleConfigurations = Files.lines(file)
            .map(line -> build(line, codeType))
            .collect(Collectors.toCollection(HashSet::new));

但这有一个关于内容的问题(仅在容器上)。它读取文件而不是整个文件,它到达给定的行(例如第 8000 行)并且没有完全读取它(在逗号分隔符之前读取大约半行)。然后我得到一个 java.lang.ArrayIndexOutOfBoundsException 因为我的构建方法尝试拆分然后行并且我访问索引 1(它没有,只有 0):

private VehicleConfiguration build(String line, CodeType codeType) {
    String[] cells = line.split(lineSeparator);
    var vc = new VehicleConfiguration();
    vc.setVin(cells[0]);
    vc.setCode(cells[1]);
    vc.setType(codeType);
    return vc;
}

可能是什么问题?我不明白相同的代码(在 Java 中)如何在 Windows 上工作,但在 Linux 容器上却不行。没有意义。

我使用的是 Java 11。文件是使用 docker-compose 文件中的卷复制的,如下所示:

    volumes:
  - ./file-sources:/file-sources

然后我将文件(在 linux 容器上使用 cp 命令)从文件源复制到 /root,因为这是应用程序监听新文件到达的地方。然后使用我描述的方法读取文件内容。示例文件数据(没有奇怪的字符):

提前致谢。

更新:尝试使用 newBufferedReader 方法,结果相同(适用于 windows,不适用于 linux 容器):

  private Set<VehicleConfiguration> loadConfigurations(Path file, CodeType codeType) throws IOException {
    String line;
    Set<VehicleConfiguration> configurations = new HashSet<>(); // this way we ignore duplicates in the same file
    try(BufferedReader br = Files.newBufferedReader(file)){
        while ((line = br.readLine()) != null)   {
            configurations.add(build(line, codeType));
        }
    }

    log.debug("Loaded " + configurations.size() + " configurations");
    return configurations;
}

linux 容器中的 wc -l(在 /root 中)返回:15185 hard_001.csv

更新:这不是解决方案,但我发现通过将文件直接放在文件源文件夹中并使该文件夹成为代码侦听的文件夹,文件被读取。所以基本上,在容器内使用 cp/mv 到另一个文件夹时,问题似乎更加明显。也许文件在完全复制/移动之前被读取,这就是它读取 0 个配置的原因?

【问题讨论】:

  • docker容器中的文件是空的吗?文件如何复制到容器中?读取的代码运行时文件是否有可能没有完全写入?
  • 就像我在主帖中所说的那样。它不是空的。我使用 head/cat/ 命令通过在容器上运行 bash 来检查它是否为空。该文件是从容器中的另一个文件夹复制的。它与 docker-compose 卷一起放在原始文件夹中。
  • 文件是从另一个文件夹复制的 - 这是什么时候/如何发生的?
  • 特别是,我看不出所呈现的代码如何记录加载的 0 个配置,除非它正在读取的文件完全为空。否则,BufferedReader 将至少读取一行,否则会引发异常,如果是前者,则 build() 将提供至少一个配置对象,否则自身会引发异常。方法中的任何地方都没有catch 块,因此如果抛出任何异常,则不会到达最后一个log.debug()
  • 附带说明,当您的参数是Path 时,您应该使用Files.newBufferedReader(Path…) 而不是通过new BufferedReader(new FileReader(file.toFile())) 强制使用默认文件系统。

标签: java docker stream java-stream bufferedreader


【解决方案1】:

在 java 中有一些你永远不应该使用的方法。永远。

new FileReader(File) 就是其中之一。

任何时候你有一个东西代表字节并且以某种方式出现字符或字符串,反之亦然?永远不要使用这些,除非所述方法的规范明确指出它总是使用预设的字符集。几乎所有此类方法都使用“系统默认字符集”,这意味着该操作取决于您运行它的机器。这是“这将失败,您的测试无法捕捉到它”的简写。你不想要的。

这就是为什么你永远不应该使用这些东西。

FileReader 已修复(有第二个构造函数采用字符集),但这只是从 JDK11 开始。您已经有了不错的新 API,为什么要切换回极旧的 File API?不要那样做。

如果您不指定,Files 中的所有各种方法,例如 Files.newBufferedReader,都指定使用 UTF-8(这样,Files 更有用,并且与大多数其他 java 核心库不同)。因此:

try (BufferedReader br = Files.newBufferedReader(file)) {

这只是..比你的线更好。

现在,它可能仍然会失败。但这很好!它会在您的开发机器上失败。实际上,您正在阅读的文件很可能不是 UTF_8 格式的。这是可能的猜测;大多数 linuxen 使用 UTF_8 默认字符集部署,而大多数开发机器没有;如果您的开发机器正在工作而您的部署环境没有,那么明显的结论是您的输入文件不是 UTF_8。它也不需要是您的开发机器的默认设置;像 ISO_8859_1 这样的东西永远不会抛出异常,但它会改为读取 gobbledygook。您的代码似乎可以正常工作(没有崩溃),但您阅读的文本仍然不正确。

找出你得到的文本编码,然后指定它。如果是 ISO_8859_1,例如:

try (BufferedReader br = Files.newBufferedReader(file, StandardCharsets.ISO_8859_1)) {

现在您的代码不再具有“适用于某些机器但不适用于其他机器”的性质。

如果需要,请在十六进制编辑器中检查失败的行。我敢打赌,甜甜圈里会有一个 0x80 或更高的字节(十进制,128 或更高)。在各种文本编码中,从 ASCII 到任何 ISO-8859 变体到 UTF-8 Windows Cp1252 到 macroman 再到许多其他东西,直到并包括 127 在内的所有内容往往意味着完全相同的东西,只要它只是纯字母和数字,编码错误不会有任何区别。但是一旦你达到 0x80 或更高,它们就完全不同了。有了那个字节 + 对它应该是什么字符的一些了解,通常是弄清楚该文本文件的编码是什么的良好开端。

注意:如果不是这样,请检查文本文件是如何从您的开发机器复制到您的部署环境的。你确定它是同一个文件吗?如果它是通过文本机制复制的,则可能再次受到字符集编码的影响,但这次是文件的写入方式,而不是您的 java 应用程序读取它的方式。

【讨论】:

  • 感谢您的回答。我会尝试你的建议。我还将添加有关该文件以及如何将其复制到主帖的更多信息。基本上,文件是使用 docker-compose yml 上的卷复制的,文件的内容只是数字和英文字母。
  • 请注意,“它没有奇怪的字符”是您应该仔细检查的。你确定吗?并非所有奇怪的角色都看起来如此。有破折号、不间断空格和十亿个其他字符,它们看起来非常相似,如果不完全相同的话,就像你可能称之为“正常”字符的东西。十六进制编辑器证明,否则您的眼球不可信。
  • 文件长达数千行。我不能确定,你是对的。我已将文件上传到在线十六进制编辑器,看看我是否能看到不寻常的东西。但是,我对此了解不多。我正在尝试检查是否有 0x80 或更高的字节,就像你说的那样。我正在使用这个:onlinehexeditor.com 我已经搜索了 0x80。什么也没找到。无论如何,我仍然必须尝试建议的方法。会尽快给予反馈。
  • 是的,我尝试了新方法,结果仍然相同(适用于开发 Windows 机器,但不适用于 linux 容器)。在主帖中添加了信息。