1. 概述

在调试或向用户显示信息时,将输出打印到控制台是常见的操作。然而,有时可能需要将控制台输出保存到文本文件中,以便进行进一步分析或文档记录。

在这篇教程中,我们将探讨如何在Java中将控制台输出重定向到文本文件。

2. 准备工作

当我们谈论将控制台输出写入文本文件时,可能会遇到两种情况:

  • 只有文件 - 将所有输出重定向到文件,不将任何内容打印到控制台。
  • 控制台和文件 - 输出写入控制台和文件。

本教程将涵盖这两种情况。

在我们进入编码部分之前,先准备一些要写入控制台的文本。为了便于测试,我们在一个字符串列表中放入三条行:

final static List<String> OUTPUT_LINES = Lists.newArrayList(
  "I came",
  "I saw",
  "I conquered");

接下来,我们将讨论如何将输出从控制台重定向到文件。因此,我们将使用单元测试断言来验证文件是否包含预期的内容。

JUnit 5的临时目录功能允许我们创建文件,将数据写入文件,并在验证后自动删除文件。因此,我们将在测试中使用它。

现在,让我们看看如何实现重定向。

3. 只将输出写入文件

通常,我们使用System.out.println()来打印文本到控制台。System.out是默认的PrintStream对象。System类提供了setOut()方法,允许我们用我们的PrintStream对象替换默认的“out”。

由于我们要将数据写入文件,我们可以创建一个PrintStream,来自FileOutputStream

接下来,我们创建一个测试方法来检查这个想法是否可行:

@Test
void whenReplacingSystemOutPrintStreamWithFileOutputStream_thenOutputsGoToFile(@TempDir Path tempDir) throws IOException {
    PrintStream originalOut = System.out;
    Path outputFilePath = tempDir.resolve("file-output.txt");
    PrintStream out = new PrintStream(Files.newOutputStream(outputFilePath), true);
    System.setOut(out);

    OUTPUT_LINES.forEach(line -> System.out.println(line));
    assertTrue(outputFilePath.toFile().exists(), "The file exists");
    assertLinesMatch(OUTPUT_LINES, Files.readAllLines(outputFilePath));
    System.setOut(originalOut);
}

在测试中,我们首先备份默认的System.out。然后,我们构造一个PrintStream对象out,它包装了一个FileOutputStream。接着,我们将默认的System.out替换为我们的“out”。

这些操作使System.out.println()将数据写入文件file-output.txt,而不是控制台。我们使用assertLinesMatch()方法验证了文件内容。

最后,我们将System.out恢复为默认值。

如果运行测试,它会通过。此外,不会有任何输出打印到控制台。

4. 创建DualPrintStream

现在,让我们看看如何将数据打印到控制台和文件。换句话说,我们需要两个PrintStream对象。

由于System.setOut()只接受一个PrintStream参数,我们不能传递两个PrintStream。但是,我们可以创建一个新的PrintStream子类来携带额外的一个PrintStream对象

class DualPrintStream extends PrintStream {
    private final PrintStream second;

    public DualPrintStream(OutputStream main, PrintStream second) {
        super(main);
        this.second = second;
    }
    
    ...
}

DualPrintStream扩展了PrintStream。此外,我们可以在构造函数中传入一个额外的PrintStream对象(第二个)。然后,我们需要覆盖PrintStreamwrite()方法,以便第二个PrintStream可以搭便车并执行相同的操作:

class DualPrintStream extends PrintStream {
    ...
    
    @Override
    public void write(byte[] b) throws IOException {
        super.write(b);
        second.write(b);
    }
}

现在,让我们检查它是否按预期工作:

@Test
void whenUsingDualPrintStream_thenOutputsGoToConsoleAndFile(@TempDir Path tempDir) throws IOException {
    PrintStream originalOut = System.out;
    Path outputFilePath = tempDir.resolve("dual-output.txt");
    DualPrintStream dualOut = new DualPrintStream(Files.newOutputStream(outputFilePath), System.out);
    System.setOut(dualOut);
    OUTPUT_LINES.forEach(line -> System.out.println(line));
    assertTrue(outputFilePath.toFile().exists(), "The file exists");
    assertLinesMatch(OUTPUT_LINES, Files.readAllLines(outputFilePath));
    System.setOut(originalOut);
}

当运行时,它通过了,这意味着文本已被写入文件。同时,我们也能在控制台上看到这三条行。

最后,值得注意的是,PrintStream还有其他方法,我们需要覆盖它们以保持文件和控制台流同步,如close(), flush(),以及write()的各种变体。我们还应该覆盖checkError()方法以优雅地处理IOExceptions

5. 总结

在这篇文章中,我们学习了如何通过替换默认的System.out使System.out.println()将数据写入文件。

如往常一样,这里展示的所有代码片段可在GitHub上找到。