JVM 單進程模型簡單高效,但特定場景下進程隔離是剛需:
| 特性 | Runtime.exec() (舊 API) | ProcessBuilder (Java 1.5+ 推薦) |
|---|---|---|
| API 風格 | 字串命令,依賴 Shell 解析 | List<String> 參數陣列,無 Shell 注入風險 |
| 環境變數 | 繼承父進程,難以定制 | Map 操作環境變數,精確控制 |
| 工作目錄 | 固定為父進程目錄 | directory(File) 指定 |
| 輸入輸出重導向 | 手動處理 Stream | redirectInput/Output/Error 直通文件/PIPE/INHERIT |
| 啟動方式 | exec() 同步阻塞至進程啟動 | start() 異步啟動,startPipeline() (Java 9+) 管道鏈 |
ProcessBuilder pb = new ProcessBuilder(
"java", "-Xmx512m", "-cp", "/app/plugins/*", "com.plugin.Main", "arg1", "arg2"
);
// 等同 pb.command(List.of("java", "-Xmx512m", ...))
Process process = pb.start(); // 異步啟動,立即返回
關鍵:參數列表化,避免字串拼接導致空格/引號/注入問題。
Map<String, String> env = pb.environment();
env.put("PLUGIN_HOME", "/opt/plugin");
env.put("JAVA_OPTS", "-Dconfig.file=/etc/plugin.conf");
env.remove("CLASSPATH"); // 清理干擾變數
pb.directory(new File("/var/lib/plugin/work")); // 隔離工作目錄
// 子進程相對路徑、臨時文件均在這裡
// 1. 直通文件 (最高效,無 Java 堆記憶體拷貝)
pb.redirectOutput(ProcessBuilder.Redirect.appendTo(new File("/var/log/plugin.out")));
pb.redirectError(ProcessBuilder.Redirect.to(new File("/var/log/plugin.err")));
// 2. 繼承父進程控制台 (調試用)
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
// 3. 管道流 (需 Java 代碼處理輸出)
Process process = pb.start();
// 非阻塞讀取輸出 (Java 8+ try-with-resources)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("[Plugin] {}", line);
}
}
Process process = pb.start();
try (OutputStream stdin = process.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stdin, StandardCharsets.UTF_8))) {
writer.write("input data line 1\n");
writer.write("input data line 2\n");
writer.flush(); // 必須 flush,否則子進程 read() 阻塞
} // try-with-resources 自動 close(),發送 EOF,子進程 read() 返回 -1
ProcessBuilder.startPipeline())// 等同 shell: cat file.txt | grep "error" | wc -l
List<ProcessBuilder> builders = List.of(
new ProcessBuilder("cat", "file.txt"),
new ProcessBuilder("grep", "error"),
new ProcessBuilder("wc", "-l")
);
List<Process> processes = ProcessBuilder.startPipeline(builders);
// 最後一個進程的輸出即為管道結果
try (BufferedReader r = new BufferedReader(new InputStreamReader(processes.get(2).getInputStream()))) {
System.out.println("Error lines: " + r.readLine());
}
// 阻塞等待
int exitCode = process.waitFor(); // 0 成功,非 0 失敗
// 超時等待 (Java 8+)
boolean finished = process.waitFor(30, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly(); // 強殺
throw new TimeoutException("Plugin execution timeout");
}
onExit())// Java 9+ 原生 CompletableFuture
CompletableFuture<Process> future = process.onExit();
future.thenAccept(proc -> {
int code = proc.exitValue();
log.info("Plugin exited with code: {}", code);
// 觸發後續邏輯:清理臨時文件、回調、發送事件
}).exceptionally(ex -> {
log.error("Plugin process error", ex);
return null;
});
// 定時任務檢查
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
if (process.isAlive()) {
// 可選:發送心跳信號、檢查資源佔用
} else {
scheduler.shutdown();
handleUnexpectedExit(process.exitValue());
}
}, 10, 10, TimeUnit.SECONDS);
| 風險 | 對策 |
|---|---|
| 殭屍進程 (父進程先退出) | Java 9+ ProcessBuilder.startPipeline 自動管理;舊版本註冊 Runtime.addShutdownHook 銷毀子進程 |
| 管道緩衝區滿導致死鎖 | 異步讀取 stdout/stderr (獨立執行緒/CompletableFuture),或重導向到文件 |
| 子進程 OOM / 惡意佔用資源 | 啟動參數 -Xmx、OS ulimit -v、cgroups memory.limit_in_bytes |
| 命令注入攻擊 | 嚴禁字串拼接命令,強制 List<String> 參數化,校驗白名單 |
| 臨時文件洩漏 | 工作目錄隔離 + 定時清理 / File.deleteOnExit() / try-with-resources |
| 編碼亂碼 | 明確指定 StandardCharsets.UTF_8,環境變數 LANG=zh_CN.UTF-8 |
public class PluginProcessManager {
private final Map<String, Process> runningPlugins = new ConcurrentHashMap<>();
public Process startPlugin(PluginDescriptor desc) throws IOException {
ProcessBuilder pb = new ProcessBuilder()
.command(buildJavaCommand(desc)) // 不同 JDK 路徑、類路徑、模組
.directory(desc.getWorkDir())
.environment(desc.getEnv())
.redirectOutput(Redirect.appendTo(desc.getLogFile()))
.redirectError(Redirect.appendTo(desc.getLogFile()));
Process process = pb.start();
runningPlugins.put(desc.getId(), process);
// 非阻塞監控
process.onExit().thenRun(() -> runningPlugins.remove(desc.getId()));
return process;
}
private List<String> buildJavaCommand(PluginDescriptor desc) {
return List.of(
desc.getJdkPath() + "/bin/java",
"-Xmx" + desc.getMaxHeapMb() + "m",
"--add-modules", "java.xml,jdk.unsupported", // 模組化隔離
"-cp", desc.getClasspath(),
desc.getMainClass()
);
}
}
總結:
ProcessBuilder是 Java 多進程的瑞士軍刀。參數列表化、I/O 重導向文件化、環境隔離、非阻塞監控、超時強殺 六大紀律落地,即可構建穩健的插件沙箱、異構系統集成、離線任務編排系統。