Java 調用外部 Shell 腳本是常見需求,但直接使用 Runtime.getRuntime().exec() 容易遇到「進程卡死」、「權限不足」、「環境變量缺失」等問題。本文提供正確姿勢與最佳實踐。
子進程的標準輸出 和錯誤輸出 由 Java 進程通過管道讀取。若 Java 端不及時讀取,管道緩衝區填滿,子進程寫入阻塞,導致 waitFor() 永久等待。**必須在調用 waitFor() 前啟動線程消費兩個流。**
public class ShellExecutor {
public static int execute(String scriptPath, String... args) throws IOException, InterruptedException {
List command = new ArrayList<>();
command.add("/bin/bash"); // 或 /bin/sh
command.add(scriptPath);
command.addAll(Arrays.asList(args));
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); // 關鍵:合併錯誤流到標準輸出,簡化處理
// pb.directory(new File("/work/dir")); // 設置工作目錄
// pb.environment().put("CUSTOM_VAR", "value"); // 設置環境變量
Process process = pb.start();
// 異步消費輸出流 (防止緩衝區填滿)
Thread outputThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[SCRIPT] " + line); // 或寫入日誌
}
} catch (IOException e) { e.printStackTrace(); }
});
outputThread.start();
int exitCode = process.waitFor(); // 等待結束
outputThread.join(1000); // 確保輸出讀取完成
return exitCode;
}
public static void main(String[] args) throws Exception {
int code = execute("/home/user/scripts/backup.sh", "arg1", "arg2");
System.out.println("腳本退出碼: " + code);
}
}
pb.redirectErrorStream(true) 避免需啟動兩個讀取線程。/bin/bash) 使用絕對路徑。chmod +x script.sh,或在命令中顯式調用 bash script.sh。pb.environment() 修改/清理。process.waitFor(timeout, TimeUnit) 避免無限等待。List 命令列表而非單一字符串,避免 Shell 注入漏洞 (參數不會被 Shell 解析)。
// 易出錯:需手動處理兩個流、參數分割、路徑解析
String cmd = "bash /path/to/script.sh arg1";
Process p = Runtime.getRuntime().exec(cmd);
// 必須啟動兩個線程分別讀取 p.getInputStream() 和 p.getErrorStream()
int exit = p.waitFor();
Permission denied:腳本無執行權限 (chmod +x) 或調用用戶無權限。No such file or directory:路徑錯誤、腳本內部命令路徑錯誤 (建議腳本內用絕對路徑)。Exit code 127:命令未找到 (PATH 環境變量問題)。InputStreamReader(..., StandardCharsets.UTF_8) 或腳本輸出編碼一致的編碼。