Java 处理流程流的正确方法

Java 处理流程流的正确方法,java,stream,processbuilder,Java,Stream,Processbuilder,有谁能澄清我,如果下面的程序是正确的方式来处理没有任何流缓冲区满和阻塞问题的流程流 我正在从java程序调用外部程序,我正在使用ProcessBuilder构建流程,并在执行 Process gpgProcess = processBuilder.start(); 我正在用一种方法处理这个过程 String executionResult = verifyExecution(gpgProcess); 在我的方法中,我试图处理流程流 private String verifyExecution

有谁能澄清我,如果下面的程序是正确的方式来处理没有任何流缓冲区满和阻塞问题的流程流

我正在从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方法。声明只有在保证读取字符不会阻塞时,该方法才会返回true。返回false并不意味着已经到达流的末尾;这只是意味着下一次读取可能会阻止含义,它可能不会立即返回

知道何时到达读取器数据流末尾的唯一方法是:

检查其读取方法是否返回负数 检查BufferedReader的readLine方法是否返回null 因此,您的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。其中一个或两个方法可能返回true,也可能不返回true,即使没有可用字符;ready方法的唯一保证是返回true意味着下一次读取不会阻塞。即使在字符流结束时,ready也可以返回true,此时下一次读取将立即返回-1,只要该读取没有阻塞

总之,根本不要使用ready。使用所有两个流,并检查错误流是否为空:

String output = readStream(stdErr);
if (output.isEmpty()) {
    String output = readStream(stdOut);
}
gpgResult = "Exit code: " + gpgProcess.exitValue() + "\n" + output;
这将解决您的问题所呈现的情况:要么进程产生标准错误,在标准输出上没有行,要么相反。但是,这通常无法正确处理流程

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

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。这样截取它可能会导致它被打印两次。

不,这不是正确的方法

首先,在某些系统上,您的代码将永远卡在gpgProcess.waitFor调用上,因为在完全读取和使用其标准输出和标准错误之前,进程无法完成

其次,您没有正确使用Reader的ready方法。声明只有在保证读取字符不会阻塞时,该方法才会返回true。返回false并不意味着已经到达流的末尾;这只是意味着下一次读取可能会阻止含义,它可能不会立即返回

知道何时到达读取器数据流末尾的唯一方法是:

检查其读取方法是否返回负数 检查BufferedReader的readLine方法是否返回null 因此,您的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。其中一个或两个方法可能返回true,也可能不返回true,即使没有可用字符;ready方法的唯一保证是返回true意味着下一次读取不会阻塞。即使在字符流结束时,ready也可以返回true,此时下一次读取将立即返回-1,只要该读取没有阻塞

总之,根本不要使用ready。使用所有两个流,并检查错误流是否为空:

String output = readStream(stdErr);
if (output.isEmpty()) {
    String output = readStream(stdOut);
}
gpgResult = "Exit code: " + gpgProcess.exitValue() + "\n" + output;
这将解决您的问题所呈现的情况:要么进程产生标准错误,在标准输出上没有行,要么相反。但是,这通常无法正确处理流程

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

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 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。像这样截取它可能会导致它被打印两次。

如果不调用读取,就无法可靠地判断流是否有可用数据,但如果没有可用数据,调用将被阻止。像available和ready这样的方法是不可靠的,因为它们会给出假阴性;他们可以报告没有可用的数据,即使有

用于任何进程的通用工具都需要一个单独的线程来使用每个InputStream。这是因为,在一般情况下,进程可以交错输出到stdout和stderr,解除一个进程的阻塞可能导致另一个进程阻塞,依此类推。进程可能会写入部分标准输出,然后阻塞写入标准错误。如果主进程只使用一个线程,它将挂起,不管它首先读取哪个流。使用这两个流的独立线程将确保进程平稳运行

如果您正在运行一个特定的进程,并且您可以保证它在每种情况下都有特定的输出,那么您可以采取一些快捷方式……请记住,快捷方式会造成长时间的延迟。

没有要读取的调用,没有可靠的方法来判断流是否有可用的数据,但如果没有可用的数据,则该调用将被阻止。像available和ready这样的方法是不可靠的,因为它们会给出假阴性;他们可以报告没有可用的数据,即使有

用于任何进程的通用工具都需要一个单独的线程来使用每个InputStream。这是因为,在一般情况下,进程可以交错输出到stdout和stderr,解除一个进程的阻塞可能导致另一个进程阻塞,依此类推。进程可能会写入部分标准输出,然后阻塞写入标准错误。如果主进程只使用一个线程,它将挂起,不管它首先读取哪个流。使用这两个流的独立线程将确保进程平稳运行


如果您正在运行一个特定的进程,并且您可以保证它在任何情况下都有特定的输出,那么您可以采取一些捷径…请记住,捷径会造成长时间的延迟。

这是不正确的。waitFor将等待进程的结束。在等待期间,没有人读取流,因此一旦其中一个管道的缓冲区已满,就会出现死锁。除此之外,如果您在进程仍在运行时修复它并读取,ready会告诉您是否至少可以读取一个字符,而不是是否有完整的行可用,因此当没有完整的行可用时,您对行的乐观读取可能会阻止您的应用程序,另一个管道正在满负荷运行,这将阻塞进程,再次导致死锁…这是不正确的。waitFor将等待进程的结束。在等待期间,没有人读取流,因此一旦其中一个管道的缓冲区已满,就会出现死锁。除此之外,如果您在进程仍在运行时修复它并读取,ready会告诉您是否至少可以读取一个字符,而不是是否有完整的行可用,因此当没有完整的行可用时,您对行的乐观读取可能会阻止您的应用程序,另一个管道正在满负荷运行,这将阻塞进程,再次导致死锁…这仍然无法解决问题。readStreamstdErr调用可能会在子进程仍在运行时等待新内容,但由于没有人在读取其标准输出,因此处于死锁状态。由于每个readStream方法调用都尝试读取,直到结束,因此它们只能在子进程终止后返回。遗憾的是,除非你逐字节轮询这两个流,否则没有正确的顺序解决方案。@Holger对于一般情况,你是绝对正确的。但这个特殊的问题似乎假设要么有错误输出,要么没有标准输出,反之亦然。如果由我决定的话,我会使用重定向错误流。对,问题似乎是这样假设的。考虑到其他不正确的假设,假设这个假设现在是正确的是非常投机的。此外,人们倾向于复制一个解决方案,并将其用于未来的任何用途,特别是在不理解甚至不讨论解决方案的局限性的情况下……这基本上是正确的,但第二段被夸大了:孩子不需要在退出前消耗掉所有的输出,它只需要使其标准输出和标准输出管道缓冲区小于满-在现代系统上通常是64k。一般来说,家长/用户应该获得所有的输出,以了解正在发生的事情。这仍然不能解决问题。readStreamstdErr调用可能会等待新内容
虽然子进程仍在运行,但处于死锁状态,因为没有人读取其标准输出。由于每个readStream方法调用都尝试读取,直到结束,因此它们只能在子进程终止后返回。遗憾的是,除非你逐字节轮询这两个流,否则没有正确的顺序解决方案。@Holger对于一般情况,你是绝对正确的。但这个特殊的问题似乎假设要么有错误输出,要么没有标准输出,反之亦然。如果由我决定的话,我会使用重定向错误流。对,问题似乎是这样假设的。考虑到其他不正确的假设,假设这个假设现在是正确的是非常投机的。此外,人们倾向于复制一个解决方案,并将其用于未来的任何用途,特别是在不理解甚至不讨论解决方案的局限性的情况下……这基本上是正确的,但第二段被夸大了:孩子不需要在退出前消耗掉所有的输出,它只需要使其标准输出和标准输出管道缓冲区小于满-在现代系统上通常是64k。通常,父/用户应该获得所有的输出,以了解正在发生的事情。