1. 行程的輸入/輸出串流
當你從 Java 啟動外部行程,你的程式與它之間會開啟一個迷你對話——三個通道。
第一個通道是標準輸出。外部行程想顯示在「螢幕」上的一切,你都可以透過 Process.getInputStream() 捕捉到。第二個通道是錯誤輸出。如果行程要抱怨或回報錯誤,你會經由 Process.getErrorStream() 得知。最後,第三個是標準輸入。若行程在等待你提供資料,你可以透過 Process.getOutputStream() 傳送給它。
名稱看起來有點讓人困惑,好像被對調了。對於行程而言,InputStream 是你讀取的東西(它的輸出),而 OutputStream 是你寫入的地方(它的輸入)。但以你的 Java 程式的視角來看就合理了:你讀的是行程輸出的內容,寫的是你想傳給行程的資料。
行程串流示意圖
+-------------------+ +---------------------+
| 你的程式 | <-------> | 外部行程 |
+-------------------+ +---------------------+
| |
| getOutputStream() --------> stdin
| getInputStream() <-------- stdout
| getErrorStream() <-------- stderr
2. 讀取行程輸出 (stdout)
來學習如何讀取外部行程輸出的內容。可能是指令結果、程式版本、成功訊息——任何都有可能。
範例 1:讀取指令 java -version 的輸出
先啟動行程,然後讀取它的輸出。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
public class ProcessOutputExample {
public static void main(String[] args) throws IOException {
// 建立行程:實際指令取決於你的作業系統!
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 讀取錯誤串流(java -version 通常寫到這裡!)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
}
}
注意! 指令 java -version 會把結果寫到 stderr,而不是一般的 stdout。這不是 bug,而是 Java 的特性。因此我們讀的是 process.getErrorStream()。對於大多數其他指令(例如 echo、ls、dir)——請使用 getInputStream()。
範例 2:讀取 echo 指令的輸出
ProcessBuilder pb = new ProcessBuilder("echo", "Hello, Java!");
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Process output: " + line);
}
}
為什麼需要 BufferedReader?
標準輸入串流是位元組串流(InputStream)。若要以「行」的方式閱讀,使用 InputStreamReader(把位元組轉為字元)搭配 BufferedReader(方便逐行讀取)。
3. 寫入行程的輸入串流 (stdin)
有時外部行程會等待我們提供資料。比如你啟動了一個要求輸入使用者名稱的指令碼,或是等待數字的計算器。
此時請使用 process.getOutputStream()——沒錯,就是這樣:你是「寫入」行程的串流。
範例 3:向行程傳送資料
假設你有個簡單的 Python 指令碼,讀取一行並原樣輸出:
echo_input.py:
# echo_input.py
line = input()
print("You said:", line)
從 Java 啟動它並送出一行字串:
import java.io.*;
public class ProcessStdinExample {
public static void main(String[] args) throws IOException {
ProcessBuilder pb = new ProcessBuilder("python", "echo_input.py");
Process process = pb.start();
// 寫入行程的 stdin
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream()))) {
writer.write("來自 Java 的問候!\n");
writer.flush();
}
// 讀取行程的 stdout
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("行程回應:" + line);
}
}
}
}
別忘了呼叫 flush()——否則資料可能卡在緩衝區,無法送到行程中。
4. 錯誤處理:讀取 stderr
外部行程不只會輸出結果,也可能抱怨人生——寫出錯誤。如果你不讀取錯誤輸出,可能會錯過重要資訊,甚至在錯誤緩衝區滿時發生卡住(deadlock)。
範例 4:讀取行程的 stderr
ProcessBuilder pb = new ProcessBuilder("java", "-version");
Process process = pb.start();
// 讀取錯誤
try (BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = errorReader.readLine()) != null) {
System.out.println("行程錯誤:" + line);
}
}
為什麼要同時讀兩個串流?
如果行程在 stdout 或 stderr 輸出大量資料,而你又不讀取,作業系統的緩衝區可能會被塞滿,行程就會「卡住」等待有人清空空間。因此好的做法是並行讀取兩個串流(至少要交替讀取)。
5. 實作:啟動指令、讀取輸出、處理錯誤
把以上內容整合成一個小工具:啟動外部指令,讀取它的標準輸出與錯誤,並把所有內容印到螢幕。
範例 5:通用指令啟動器
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class RunCommand {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("請提供要執行的指令,例如:java RunCommand ls -l");
return;
}
var process = new ProcessBuilder(args).start();
// 分別啟動執行緒讀取 stdout 與 stderr
var t1 = Thread.ofPlatform().start(() -> readStream(process.inputReader(), "[stdout]"));
var t2 = Thread.ofPlatform().start(() -> readStream(process.errorReader(), "[stderr]"));
int exitCode = process.waitFor(); // 等待行程結束
t1.join();
t2.join();
System.out.println("行程以代碼結束:" + exitCode);
}
private static void readStream(BufferedReader reader, String prefix) {
try (reader) {
reader.lines().forEach(line -> System.out.println(prefix + " " + line));
} catch (Exception e) {
System.err.println("讀取錯誤 " + prefix + ": " + e.getMessage());
}
}
}
這樣的做法可以避免 deadlock,也不會漏掉任何錯誤!
6. 關於編碼的簡述
預設情況下,InputStreamReader 會使用系統編碼。如果外部行程使用不同的編碼(例如它是 UTF-8,而你的系統是 Windows-1251),輸出可能會變成亂碼。請顯式指定編碼:
new InputStreamReader(process.getInputStream(), "UTF-8")
7. 範例:啟動 Python 指令碼並向其傳送資料
假設你有個 Python 指令碼,會在輸入中等待數行,然後輸出結果。
adder.py:
# adder.py
a = int(input())
b = int(input())
print(a + b)
在 Java 中,你可以把數字傳給它並讀回回應:
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
public class PythonAdder {
public static void main(String[] args) throws Exception {
var process = new ProcessBuilder("python", "adder.py").start();
// 將兩個數字寫入 Python 指令碼的 stdin
try (var writer = process.outputWriter(StandardCharsets.UTF_8)) {
writer.println("10");
writer.println("25");
}
// 使用 inputReader 從 stdout 讀取結果
try (BufferedReader reader = process.inputReader(StandardCharsets.UTF_8)) {
reader.lines().forEach(line -> System.out.println("來自 Python 的回覆:" + line));
}
int exitCode = process.waitFor();
System.out.println("Python 已結束,代碼:" + exitCode);
}
}
8. 為什麼會 deadlock?如何避免
Deadlock 指的是行程與你的程式彼此無限等待。例如,若行程的輸出緩衝區已滿,而你又不讀取它,行程就會停住。或者你在等待行程結束,而它則在等待你提供輸入——這也是 deadlock。
如何避免:
- 務必同時讀取兩個串流(stdout 與 stderr)。
- 對於長時間或輸出很多的行程,使用獨立執行緒來讀取。
- 不要在同一條執行緒中同時阻塞寫入與讀取。
9. 操作行程輸入/輸出串流時的常見錯誤
錯誤 1:只讀其中一個串流(stdout 或 stderr)。
如果只讀 stdout,而行程把錯誤寫到 stderr,錯誤緩衝區可能會滿——行程就會卡住。做法:同時讀取兩個串流(最好用獨立執行緒)。
錯誤 2:使用了錯誤的編碼。
如果沒有指定正確的編碼,輸出可能難以閱讀。務必確認外部行程使用的編碼(例如明確指定 "UTF-8",或使用 StandardCharsets.UTF_8)。
錯誤 3:未關閉串流。
未關閉的串流可能導致資源洩漏。請使用 try-with-resources,或明確關閉所有串流。
錯誤 4:在寫入行程 stdin 時未呼叫 flush()。
如果忘了呼叫 flush(),資料可能無法送達行程——它會一直等待輸入。
錯誤 5:平台差異。
指令與語法在 Windows 與 Linux/Mac 之間可能不同。請透過 System.getProperty("os.name") 檢查作業系統,並選擇對應的指令。
錯誤 6:未處理例外。
處理行程時常會拋出 IOException。務必處理例外,避免程式異常終止。
GO TO FULL VERSION