核心 API 對比
| 類別 | 特點 | 適用場景 |
Runtime.exec() | 簡單、字串命令、Shell 解析 | 簡單命令、相容舊代碼 |
ProcessBuilder | 列表參數、環境變數控制、目錄設定、流重導向 | 複雜命令、生產環境推薦 |
三大流處理鐵律(違反必死鎖)
- 必須讀取 stdout:緩衝區滿 → 子進程阻塞
- 必須讀取 stderr:同理,或
redirectErrorStream(true) 合併
- 必要時關閉 stdin:子進程等待輸入 → 掛起
標準正確範本
public static ProcessResult execute(String... command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // 合併 stderr 到 stdout
Process process = pb.start();
// 關鍵:若不需輸入,立即關閉 stdin
process.getOutputStream().close();
// 異步讀取輸出 (避免緩衝區滿阻塞)
StringBuilder output = new StringBuilder();
Thread reader = new Thread(() -> {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
output.append(line).append('\n');
}
} catch (IOException e) { /* log */ }
});
reader.start();
int exitCode = process.waitFor();
reader.join(1000); // 等待讀取完成
return new ProcessResult(exitCode, output.toString());
}
實戰案例:WMIC 重啟服務
問題重現
// 現象:程式掛起不退出,但服務實際重啟成功
Process p = Runtime.getRuntime().exec("wmic service where name='Tomcat' call startservice");
三階段排查
| 階段 | 現象 | 原因 | 解決 |
| 1. 僅讀 stdout | 阻塞在 readLine() | stderr 緩衝區滿 | pb.redirectErrorStream(true) |
| 2. 合併流 | 仍阻塞 | WMIC 進入互動模式,等待 stdin | process.getOutputStream().close() |
| 3. 關閉 stdin | 正常退出 | EOF 讓 WMIC 退出互動模式 | 完美解決 |
最終代碼
ProcessBuilder pb = new ProcessBuilder("wmic", "service", "where", "name='Tomcat'", "call", "startservice");
pb.redirectErrorStream(true);
Process p = pb.start();
p.getOutputStream().close(); // 關鍵!
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) System.out.println(line);
}
int exitCode = p.waitFor();
ProcessBuilder 參數解析陷阱
空格處理邏輯
// ProcessBuilder(List) 內部實現:
// 1. 列表每個元素若含空格 → 加雙引號
// 2. 用空格連接所有元素
// 問題案例:
List cmd = Arrays.asList("wmic", "service", "where", "name='My Service'", "call", "startservice");
// 實際執行命令:
wmic service where "name='My Service'" call startservice
// WMIC 解析失敗:雙引號干擾單引號
解決方案
- 改用 Runtime.exec(String):完整命令字串,自行控制引號
- 避免參數含空格:服務名不含空格
- 手工構建命令字串:
String cmd = "wmic service where name='My Service' call startservice";
進階模式:超時控制與非同步執行
JDK 9+ Process API
// 支援超時
Process process = pb.start();
boolean finished = process.waitFor(30, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly().waitFor();
throw new TimeoutException("命令執行超時");
}
CompletableFuture 非同步
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try { return execute(cmd); }
catch (Exception e) { throw new CompletionException(e); }
});
// 後續處理
future.thenAccept(result -> {
if (result.exitCode != 0) alert(result.output);
});
安全性警示
- 命令注入:嚴禁拼接用戶輸入
"cmd " + userInput
- 參數化:使用
ProcessBuilder(List) 列表形式,避免 Shell 解析
- 白名單驗證:僅允許預定義命令與參數
- 最小權限:執行用戶非 root/Administrator
總結
Java 執行外部命令核心在於三流管理:stdout/stderr 必讀、stdin 必關。ProcessBuilder 功能強大但參數解析有陷阱,複雜命令可回退 Runtime.exec(String)。生產環境必加超時控制、非同步讀取、安全白名單。現代專案優先考慮純 Java 替代方案 (如 JNA 調用 DLL、REST API、SSH 庫),減少外部進程依賴。