处理流程流的正确方法

The correct way to handle Process streams

任何人都可以澄清我下面的过程是否是处理进程流的正确方法而没有任何流缓冲区已满和阻塞问题

我正在从 java 程序调用外部程序,我正在使用 ProcessBuilder 构建流程并在我执行

Process gpgProcess = processBuilder.start();

我正在使用一种方法处理流程

String executionResult = verifyExecution(gpgProcess);

在我的方法中,我尝试处理流程流

private String verifyExecution(Process gpgProcess) throws IOException, InterruptedException {

        String gpgResult = null;

        BufferedReader stdOut = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()));
        BufferedReader stdErr = new BufferedReader(new InputStreamReader(gpgProcess.getErrorStream()));

        gpgProcess.waitFor();

        if(stdErr.ready()) {
            gpgResult = "Exit code: " + gpgProcess.exitValue() + "\n" + readStream(stdErr);
        } else if(stdOut.ready()) {
            gpgResult = "Exit code: " + gpgProcess.exitValue() + "\n" + readStream(stdOut);
        } else {
            gpgResult = "Exit code: " + gpgProcess.exitValue();
        }


        int exitCode = gpgProcess.exitValue();  
        this.setExitCode(exitCode);
        stdOut.close();
        stdErr.close();

        if(exitCode != 0) {
            throw new RuntimeException("Pgp Exception: " + gpgResult);
        }

        return gpgResult;
    }

readStream 方法用于读取我的流文本。

private String readStream(BufferedReader reader) throws IOException {

        StringBuilder result = new StringBuilder();

        try {
            while(reader.ready()) {
                result.append(reader.readLine());
                if(reader.ready()) {
                    result.append("\n");
                }
            }
        } catch(IOException ioe) {
            System.err.println("Error while reading the stream: " + ioe.getMessage());
            throw ioe;
        }

        return result.toString();
    }

不,这不是正确的做法。

首先,在某些系统上,您的代码将永远停留在 gpgProcess.waitFor() 调用上,因为在其标准输出和标准错误被完全读取和消耗之前,该过程无法完成。

其次,您没有正确使用 Reader 的 ready() 方法。 documentation 声明方法 return 只有在保证读取字符不会阻塞时才为真。返回 false 并不 表示已到达流的末尾;这只是意味着下一次读取可能会阻塞(意思是,它可能不会立即return)。

只有 方法可以知道您何时到达 Reader 数据流的末尾:

  • 检查它的任何 read 方法是否 return 为负数
  • 检查BufferedReader的readLine方法是否为空return

因此您的 readStream 方法应如下所示:

String line;
while ((line = reader.readLine()) != null) {
    result.append(line).append("\n");
}

从 Java 8 开始,您可以使其更短:

return reader.lines().collect(Collectors.joining("\n"));

同样,您不应该调用 stdErr.ready()stdOut.ready()。即使没有可用字符,其中一种或两种方法都可能 return 为真,也可能不为真; ready() 方法的唯一保证是 return 为真意味着下一次读取不会阻塞。即使在字符流的末尾,ready() 也可以 return 为真,此时下一次读取将立即 return -1,只要该读取不阻塞。

总而言之,根本不要使用 ready()。消费两个流,检查错误流是否为空:

String output = readStream(stdErr);
if (output.isEmpty()) {
    String output = readStream(stdOut);
}
gpgResult = "Exit code: " + gpgProcess.exitValue() + "\n" + output;

这将解决您的问题似乎出现的情况:进程产生标准错误并且标准输出中没有行,或者相反。但是,这通常无法正确处理进程。

对于一般情况,最简单的解决方案是让进程使用 redirectErrorStream 将其标准错误与标准输出合并,因此只有一个流可以使用:

processBuilder.redirectErrorStream(true);
Process gpgProcess = processBuilder.start();

verifyExecution 方法可以包含:

String output;
try (BufferedReader stdOut = new BufferedReader(new InputStreamReader(gpgProcess.getInputStream()))) {
    output = readStream(stdOut);
}

if (output.isEmpty()) {
    gpgResult = "Exit code: " + gpgProcess.waitFor();
} else {
    gpgResult = "Exit code: " + gpgProcess.waitFor() + "\n" + output;
}

如果你绝对必须有独立的标准错误和标准输出,你至少需要一个后台线程。我发现 ExecutorService 使从后台线程传递值变得更容易:

ExecutorService background = Executors.newSingleThreadExecutor();
Future<String> stdOutReader = background.submit(() -> readStream(stdOut));

String output = readStream(stdErr);
if (output.isEmpty()) {
    output = stdOutReader.get();
}

background.shutdown();

if (output.isEmpty()) {
    gpgResult = "Exit code: " + gpgProcess.waitFor();
} else {
    gpgResult = "Exit code: " + gpgProcess.waitFor() + "\n" + output;
}

最后,你不应该为了打印出来而捕获并重新抛出 IOException。无论如何调用 verifyExecution 的代码都必须捕获 IOException;打印、记录或以其他方式处理 IOException 是该代码的工作。像这样拦截它可能会导致它被打印两次。

没有 可靠的 方法可以在不调用 read() 的情况下判断流是否有可用数据——但如果没有可用数据,该调用将被阻止。 available()ready() 之类的方法不可靠,因为它们会给出假阴性;他们可以报告没有可用数据,即使有。

适用于任何进程的通用工具需要一个单独的线程来使用每个 InputStream。这是因为,一般来说,进程可以将输出交织到 stdout 和 stderr,解除阻塞一个可能会导致另一个阻塞,等等。该进程可能会写入部分标准输出,然后在写入标准错误时阻塞。如果你的主进程只使用一个线程,它就会挂起,不管它先读取哪个流。使用两个流的独立线程将确保进程顺利运行。

如果你是运行一个特定的进程,并且你可以保证它在每种情况下都有一定的输出,你可以走一些捷径......记住,"Short cuts make long delays."