Java 原生 Runtime.getRuntime().exec() 雖能啟動子進程,但缺乏超時控制、輸出流非阻塞讀取、錯誤流處理、退出碼統一封裝等工程化能力。直接使用容易導致進程掛起、記憶體洩漏、結果丟失等問題。本文提供一個生產就緒的 CommandHelper 工具類,解決上述痛點。
public class CommandResult {
public static final int EXIT_VALUE_TIMEOUT = -1;
private String output; // 標準輸出
private String error; // 錯誤輸出
private int exitValue; // 退出碼
// getter/setter 省略
}
public class CommandHelper {
public static int DEFAULT_TIMEOUT = 30000; // 預設 30 秒
public static final int DEFAULT_INTERVAL = 1000;
private static long START;
public static CommandResult exec(String command)
throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(command);
CommandResult result = wait(process);
if (process != null) process.destroy();
return result;
}
private static CommandResult wait(Process process)
throws InterruptedException, IOException {
// 非阻塞讀取 stdout/stderr,避免緩衝區滿導致死鎖
BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream()));
BufferedReader inputReader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
START = System.currentTimeMillis();
boolean finished = false;
while (true) {
// 超時檢查
if (System.currentTimeMillis() - START >= DEFAULT_TIMEOUT) {
CommandResult r = new CommandResult();
r.setExitValue(EXIT_VALUE_TIMEOUT);
r.setOutput("Command process timeout");
return r;
}
// 嘗試獲取退出碼,判斷進程是否結束
try {
int exitVal = process.exitValue(); // 若未結束拋 IllegalThreadStateException
finished = true;
} catch (IllegalThreadStateException e) {
finished = false;
Thread.sleep(DEFAULT_INTERVAL);
continue;
}
if (finished) {
CommandResult r = new CommandResult();
r.setExitValue(process.waitFor());
// 讀取錯誤流
if (errorReader.ready()) {
StringBuilder eb = new StringBuilder();
String line;
while ((line = errorReader.readLine()) != null) eb.append(line).append("\n");
r.setError(eb.toString());
}
// 讀取標準輸出
if (inputReader.ready()) {
StringBuilder ob = new StringBuilder();
String line;
while ((line = inputReader.readLine()) != null) ob.append(line).append("\n");
r.setOutput(ob.toString());
}
return r;
}
}
}
}
public static void main(String[] args) {
try {
int timeout = Integer.parseInt(args[0]); // 傳入超時毫秒數
CommandHelper.DEFAULT_TIMEOUT = timeout;
CommandResult result = CommandHelper.exec("mkdir testdir");
if (result != null) {
System.out.println("Output: " + result.getOutput());
System.out.println("Error: " + result.getError());
System.out.println("Exit: " + result.getExitValue());
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| SSH 遠端命令需密碼互動 | 預設 SSH 要求交互式輸入密碼 | 使用 sshpass -p password ssh user@host 'cmd' 或配置免密鑰登入 |
| IDE 直接 Run 失敗,提示權限不足 | IDE 進程無終端環境,無法分配 TTY | 打包為 JAR,在終端執行 java -jar xxx.jar |
| 部分命令(如 cd、source)無效 | 非內建命令、需 Shell 內建指令 | 改用 bash -c "cmd1; cmd2" 或引入 Ganymed SSH-2 / JSch 庫 |
ProcessBuilder.environment()ProcessBuilder.directory(File)CompletableFuture 實現非阻塞調用ProcessBuilder.redirectOutput()CommandHelper 以 50 行核心代碼 解決了超時控制、流讀取死鎖、結果封裝三大核心問題,適合作為專案基礎設施庫的基石。對於複雜的 SSH 自動化場景,建議直接使用 JSch 或 Apache MINA SSHD 等成熟庫,避免重複造輪子。