[英]ProcessBuilder: Forwarding stdout and stderr of started processes without blocking the main thread
我正在使用 ProcessBuilder 在 Java 中構建一個進程,如下所示:
ProcessBuilder pb = new ProcessBuilder()
.command("somecommand", "arg1", "arg2")
.redirectErrorStream(true);
Process p = pb.start();
InputStream stdOut = p.getInputStream();
現在我的問題如下:我想捕獲通過該進程的 stdout 和/或 stderr 的任何內容,並將其異步重定向到System.out
。 我希望進程及其輸出重定向在后台運行。 到目前為止,我發現這樣做的唯一方法是手動生成一個新線程,該線程將連續從stdOut
讀取,然后調用System.out
的適當write()
方法。
new Thread(new Runnable(){
public void run(){
byte[] buffer = new byte[8192];
int len = -1;
while((len = stdOut.read(buffer)) > 0){
System.out.write(buffer, 0, len);
}
}
}).start();
雖然這種方法有效,但感覺有點臟。 最重要的是,它給了我一個線程來正確管理和終止。 有沒有更好的方法來做到這一點?
使用ProcessBuilder.inheritIO
,它將子進程標准 I/O 的源和目標設置為與當前 Java 進程相同。
Process p = new ProcessBuilder().inheritIO().command("command1").start();
如果 Java 7 不是一個選項
public static void main(String[] args) throws Exception {
Process p = Runtime.getRuntime().exec("cmd /c dir");
inheritIO(p.getInputStream(), System.out);
inheritIO(p.getErrorStream(), System.err);
}
private static void inheritIO(final InputStream src, final PrintStream dest) {
new Thread(new Runnable() {
public void run() {
Scanner sc = new Scanner(src);
while (sc.hasNextLine()) {
dest.println(sc.nextLine());
}
}
}).start();
}
當子進程完成時,線程將自動死亡,因為src
將 EOF。
對於Java 7 及更高版本,請參閱 Evgeniy Dorofeev 的回答。
對於Java 6 及更早版本,創建並使用StreamGobbler
:
StreamGobbler errorGobbler =
new StreamGobbler(p.getErrorStream(), "ERROR");
// any output?
StreamGobbler outputGobbler =
new StreamGobbler(p.getInputStream(), "OUTPUT");
// start gobblers
outputGobbler.start();
errorGobbler.start();
...
private class StreamGobbler extends Thread {
InputStream is;
String type;
private StreamGobbler(InputStream is, String type) {
this.is = is;
this.type = type;
}
@Override
public void run() {
try {
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line = null;
while ((line = br.readLine()) != null)
System.out.println(type + "> " + line);
}
catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
使用 Java 8 lambda 的靈活解決方案,可讓您提供一個Consumer
來逐行處理輸出(例如記錄它)。 run()
是單行的,沒有拋出已檢查的異常。 替代實現Runnable
,它可以像其他答案建議的那樣擴展Thread
。
class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumeInputLine;
public StreamGobbler(InputStream inputStream, Consumer<String> consumeInputLine) {
this.inputStream = inputStream;
this.consumeInputLine = consumeInputLine;
}
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumeInputLine);
}
}
然后你可以像這樣使用它:
public void runProcessWithGobblers() throws IOException, InterruptedException {
Process p = new ProcessBuilder("...").start();
Logger logger = LoggerFactory.getLogger(getClass());
StreamGobbler outputGobbler = new StreamGobbler(p.getInputStream(), System.out::println);
StreamGobbler errorGobbler = new StreamGobbler(p.getErrorStream(), logger::error);
new Thread(outputGobbler).start();
new Thread(errorGobbler).start();
p.waitFor();
}
這里輸出流被重定向到System.out
並且錯誤流被記錄logger
記錄在錯誤級別。
這很簡單,如下所示:
File logFile = new File(...);
ProcessBuilder pb = new ProcessBuilder()
.command("somecommand", "arg1", "arg2")
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(logFile);
通過 .redirectErrorStream(true) 告訴進程合並錯誤和輸出流,然后通過 .redirectOutput(file) 將合並的輸出重定向到文件。
更新:
我確實設法做到這一點如下:
public static void main(String[] args) {
// Async part
Runnable r = () -> {
ProcessBuilder pb = new ProcessBuilder().command("...");
// Merge System.err and System.out
pb.redirectErrorStream(true);
// Inherit System.out as redirect output stream
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
try {
pb.start();
} catch (IOException e) {
e.printStackTrace();
}
};
new Thread(r, "asyncOut").start();
// here goes your main part
}
現在您可以在System.out 中看到 main 和 asyncOut 線程的輸出
使用CompletableFuture
捕獲輸出和反應處理的簡單 java8 解決方案:
static CompletableFuture<String> readOutStream(InputStream is) {
return CompletableFuture.supplyAsync(() -> {
try (
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
) {
StringBuilder res = new StringBuilder();
String inputLine;
while ((inputLine = br.readLine()) != null) {
res.append(inputLine).append(System.lineSeparator());
}
return res.toString();
} catch (Throwable e) {
throw new RuntimeException("problem with executing program", e);
}
});
}
以及用法:
Process p = Runtime.getRuntime().exec(cmd);
CompletableFuture<String> soutFut = readOutStream(p.getInputStream());
CompletableFuture<String> serrFut = readOutStream(p.getErrorStream());
CompletableFuture<String> resultFut =
soutFut.thenCombine(serrFut, (stdout, stderr) -> {
// print to current stderr the stderr of process and return the stdout
System.err.println(stderr);
return stdout;
});
// get stdout once ready, blocking
String result = resultFut.get();
有一個庫提供了更好的 ProcessBuilder,zt-exec。 這個庫可以完全滿足您的要求,甚至更多。
以下是使用 zt-exec 而不是 ProcessBuilder 的代碼:
添加依賴項:
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-exec</artifactId>
<version>1.11</version>
</dependency>
代碼 :
new ProcessExecutor()
.command("somecommand", "arg1", "arg2")
.redirectOutput(System.out)
.redirectError(System.err)
.execute();
該庫的文檔在這里: https : //github.com/zeroturnaround/zt-exec/
我也只能使用 Java 6。我使用了 @EvgeniyDorofeev 的線程掃描器實現。 在我的代碼中,在一個進程完成后,我必須立即執行另外兩個進程,每個進程都比較重定向的輸出(基於差異的單元測試,以確保 stdout 和 stderr 與祝福的相同)。
即使我 waitFor() 進程完成,掃描器線程也不會很快完成。 為了使代碼正常工作,我必須確保在進程完成后加入線程。
public static int runRedirect (String[] args, String stdout_redirect_to, String stderr_redirect_to) throws IOException, InterruptedException {
ProcessBuilder b = new ProcessBuilder().command(args);
Process p = b.start();
Thread ot = null;
PrintStream out = null;
if (stdout_redirect_to != null) {
out = new PrintStream(new BufferedOutputStream(new FileOutputStream(stdout_redirect_to)));
ot = inheritIO(p.getInputStream(), out);
ot.start();
}
Thread et = null;
PrintStream err = null;
if (stderr_redirect_to != null) {
err = new PrintStream(new BufferedOutputStream(new FileOutputStream(stderr_redirect_to)));
et = inheritIO(p.getErrorStream(), err);
et.start();
}
p.waitFor(); // ensure the process finishes before proceeding
if (ot != null)
ot.join(); // ensure the thread finishes before proceeding
if (et != null)
et.join(); // ensure the thread finishes before proceeding
int rc = p.exitValue();
return rc;
}
private static Thread inheritIO (final InputStream src, final PrintStream dest) {
return new Thread(new Runnable() {
public void run() {
Scanner sc = new Scanner(src);
while (sc.hasNextLine())
dest.println(sc.nextLine());
dest.flush();
}
});
}
作為 msangel答案的補充,我想添加以下代碼塊:
private static CompletableFuture<Boolean> redirectToLogger(final InputStream inputStream, final Consumer<String> logLineConsumer) {
return CompletableFuture.supplyAsync(() -> {
try (
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
) {
String line = null;
while((line = bufferedReader.readLine()) != null) {
logLineConsumer.accept(line);
}
return true;
} catch (IOException e) {
return false;
}
});
}
它允許將進程的輸入流(stdout、stderr)重定向到其他一些消費者。 這可能是 System.out::println 或任何其他消耗字符串的東西。
用法:
...
Process process = processBuilder.start()
CompletableFuture<Boolean> stdOutRes = redirectToLogger(process.getInputStream(), System.out::println);
CompletableFuture<Boolean> stdErrRes = redirectToLogger(process.getErrorStream(), System.out::println);
System.out.println(stdOutRes.get());
System.out.println(stdErrRes.get());
System.out.println(process.waitFor());
Thread thread = new Thread(() -> {
new BufferedReader(
new InputStreamReader(inputStream,
StandardCharsets.UTF_8))
.lines().forEach(...);
});
thread.start();
您的自定義代碼代替...
令我驚訝的是ProcessBuilder
中的重定向方法不接受OutputStream
,只接受File
。 Java 強迫您編寫的強制樣板代碼的另一個證明。
也就是說,讓我們看一下綜合選項列表:
inheritIO
將完成這項工作。redirect*(file)
。InputStream
。 查看使用Runnable
或CompletableFuture
的答案。 您還可以修改下面的代碼來執行此操作。PrintWriter
,這可能是也可能不是標准輸出(對於測試非常有用),您可以執行以下操作:static int execute(List<String> args, PrintWriter out) {
ProcessBuilder builder = new ProcessBuilder()
.command(args)
.redirectErrorStream(true);
Process process = null;
boolean complete = false;
try {
process = builder.start();
redirectOut(process.getInputStream(), out)
.orTimeout(TIMEOUT, TimeUnit.SECONDS);
complete = process.waitFor(TIMEOUT, TimeUnit.SECONDS);
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (InterruptedException e) {
LOG.warn("Thread was interrupted", e);
} finally {
if (process != null && !complete) {
LOG.warn("Process {} didn't finish within {} seconds", args.get(0), TIMEOUT);
process = process.destroyForcibly();
}
}
return process != null ? process.exitValue() : 1;
}
private static CompletableFuture<Void> redirectOut(InputStream in, PrintWriter out) {
return CompletableFuture.runAsync(() -> {
try (
InputStreamReader inputStreamReader = new InputStreamReader(in);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader)
) {
bufferedReader.lines()
.forEach(out::println);
} catch (IOException e) {
LOG.error("Failed to redirect process output", e);
}
});
}
到目前為止,上述代碼相對於其他答案的優勢:
redirectErrorStream(true)
將錯誤流重定向到輸出流,這樣我們只需要考慮一個。CompletableFuture.runAsync
從ForkJoinPool
運行。 請注意,此代碼不會通過在CompletableFuture
上調用get
或join
來阻止,而是在其完成時設置超時(Java 9+)。 不需要CompletableFuture.supplyAsync
因為從方法redirectOut
沒有什么可返回的。BufferedReader.lines
比使用while
循環更簡單。import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws Exception {
ProcessBuilder pb = new ProcessBuilder("script.bat");
pb.redirectErrorStream(true);
Process p = pb.start();
BufferedReader logReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String logLine = null;
while ( (logLine = logReader.readLine()) != null) {
System.out.println("Script output: " + logLine);
}
}
}
通過使用這一行: pb.redirectErrorStream(true);
我們可以結合 InputStream 和 ErrorStream
默認情況下,創建的子進程沒有自己的終端或控制台。 它的所有標准 I/O(即 stdin、stdout、stderr)操作都將被重定向到父進程,在那里可以通過使用 getOutputStream()、getInputStream() 和 getErrorStream() 方法獲得的流訪問它們。 父進程使用這些流向子進程提供輸入和從子進程獲取輸出。 由於一些原生平台只為標准輸入輸出流提供有限的緩沖區大小,如果不能及時寫入輸入流或讀取子進程的輸出流,可能會導致子進程阻塞,甚至死鎖。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.