Java 調用外部程式 (如 WinRAR、7-Zip、tar) 進行檔案壓縮是常見需求。許多開發者直接拼接命令字串傳給 `Runtime.getRuntime().exec(String)`,卻在 Windows Server 2008+、含空格路徑、特殊字元場景下踩坑。本文深度解析 字串參數 vs 陣列參數 的本質區別,提供跨平台、抗空格、防注入的最佳實踐。
// 看似直觀,實則暗藏殺機
String cmd = "C:/Program Files (x86)/WinRAR/rar.exe a c:/test.rar c:/test.log";
Process proc = Runtime.getRuntime().exec(cmd);
| 方法簽名 | 解析機制 | 適用場景 |
|---|---|---|
exec(String command) | JVM 內部使用 StringTokenizer 按空格簡單分割,**不支援引號包裹、轉義、管道、重導向** 等 Shell 特性。 | 極簡命令、無參數、路徑無空格 |
exec(String[] cmdarray) | **直接將陣列元素作為 argv[] 傳給 OS 執行體**,繞過 JVM 分割,**保留每個參數原貌**,支援空格、特殊字元。 | 所有生產環境場景 (強烈推薦) |
exec(String[], Env[], File dir) | 陣列版 + 自定義環境變數 + 工作目錄 | 需隔離環境、指定工作目錄 |
// 將命令拆解為獨立參數元素,路徑含空格無需加引號!
String[] cmd = {
"C:/Program Files (x86)/WinRAR/rar.exe", // 可執行檔完整路徑
"a", // WinRAR 參數:添加到壓縮檔
"C:/test.rar", // 目標壓縮檔
"c:/test.log" // 來源檔案
};
Process proc = Runtime.getRuntime().exec(cmd);
OS 執行體 (Windows `CreateProcess` / Linux `execve`) 接收的是 **argv 陣列**。陣列第 0 個元素是程式路徑,後續是參數。Java 陣列版 `exec` 直接映射到 argv,**每個元素獨立傳遞,空格屬於路徑一部分,不再是分隔符**。
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ProcessExecutor {
/**
* 執行外部命令,捕獲輸出/錯誤流,支援超時控制
* @param command 陣列命令
* @param timeoutSeconds 超時秒數 (0 表示無限等待)
* @return 執行結果
*/
public static ExecResult execute(String[] command, int timeoutSeconds) {
ProcessBuilder pb = new ProcessBuilder(command);
// 合併錯誤流到標準輸出,避免雙流閱讀死鎖
pb.redirectErrorStream(true);
// 可選:設定工作目錄、環境變數
// pb.directory(new File("/tmp"));
// pb.environment().put("LANG", "en_US.UTF-8");
try {
Process process = pb.start();
// 異步讀取輸出 (避免緩衝區滿導致阻塞)
StringBuilder output = new StringBuilder();
Thread readerThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append('\n');
}
} catch (IOException ignored) {}
});
readerThread.start();
// 等待結束或超時
boolean finished = timeoutSeconds > 0
? process.waitFor(timeoutSeconds, TimeUnit.SECONDS)
: (process.waitFor(), true); // Java 8+ waitFor() 無超時版本
readerThread.join(1000); // 等讀取線程結束
if (!finished) {
process.destroyForcibly(); // 強制殺掉進程樹
return new ExecResult(-1, output.toString(), "Timeout after " + timeoutSeconds + "s");
}
return new ExecResult(process.exitValue(), output.toString(), null);
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
return new ExecResult(-1, "", e.getMessage());
}
}
// 結果封裝
public static class ExecResult {
public final int exitCode;
public final String output;
public final String error;
public ExecResult(int exitCode, String output, String error) {
this.exitCode = exitCode; this.output = output; this.error = error;
}
public boolean isSuccess() { return exitCode == 0; }
}
}
// 壓縮
String[] zipCmd = {"C:/Program Files/7-Zip/7z.exe", "a", "-tzip", "D:/backup.zip", "D:/data"};
ExecResult r = ProcessExecutor.execute(zipCmd, 60);
if (r.isSuccess()) { log.info("壓縮成功: {}", r.output); }
else { log.error("壓縮失敗: code={}, err={}, out={}", r.exitCode, r.error, r.output); }
// 解壓 (跨平台 Linux/macOS/Windows 通用)
String[] unzipCmd = {"/usr/bin/unzip", "-o", "/tmp/data.zip", "-d", "/tmp/out"};
// Windows 可用 7z.exe x -o"dir" file.zip
| 工具 | 平台 | 優點 | 缺點 | Java 呼叫範例 |
|---|---|---|---|---|
| 7-Zip (7z.exe / 7za) | Win/Linux/macOS | 開源、免費、格式支援最全 (zip, 7z, tar, gz, rar 解壓)、命令列一致 | 需額外安裝 | `7z a -tzip out.zip in/` |
| WinRAR (rar.exe) | Windows | RAR 格式壓縮率高、Windows 常預裝 | 商業軟體、Linux 需 Wine、命令列參數較複雜 | `rar a -r out.rar in\` |
| 系統內建 (tar/gzip/zip/unzip) | Linux/macOS | 無依賴、標準化、管道流式處理 | Windows 原生無 (WSL 除外) | `tar -czf out.tar.gz in/` |
| 純 Java 庫 (推薦) | 全平台 | 無外部依賴、進程隔離、效能可控、支援流式/加密/分卷 | 大檔案稍慢於原生工具 | Apache Commons Compress, Zip4j, zstd-jni |
架構師建議:除非極度依賴特定壓縮格式/參數,**優先使用純 Java 庫 (如 Zip4j 支援 AES 加密、分卷、流式)**。避免進程啟動開銷、路徑依賴、權限問題、跨平台不一致。僅在必須調用外部工具時,使用 `ProcessBuilder` + 陣列參數 + 超時控制 + 流異步讀取。
// Maven: net.lingala.zip4j:zip4j:2.11.+
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.CompressionMethod;
import net.lingala.zip4j.model.enums.EncryptionMethod;
public class Zip4jDemo {
public static void createEncryptedZip(String srcDir, String zipPath, String password) {
ZipParameters params = new ZipParameters();
params.setCompressionMethod(CompressionMethod.DEFLATE);
params.setCompressionLevel(CompressionLevel.NORMAL);
params.setEncryptionMethod(EncryptionMethod.AES_256);
params.setEncryptFiles(true);
params.setPassword(password.toCharArray());
try (ZipFile zipFile = new ZipFile(zipPath, password.toCharArray())) {
zipFile.addFolder(srcDir, params); // 一行搞定資料夾遞歸壓縮+加密
} catch (Exception e) {
throw new RuntimeException("壓縮失敗", e);
}
}
}