ProcessBuilder
ProcessBuilder 是 Java 用于创建操作系统进程的类,位于 java.lang 包。相比早期的 Runtime.exec(),它提供了更清晰的 API 来配置命令、环境变量、工作目录和 I/O 重定向。
基本用法
// 创建并启动进程
ProcessBuilder pb = new ProcessBuilder("ls", "-la");
Process process = pb.start();
// 等待执行完毕并获取退出码
int exitCode = process.waitFor();
System.out.println("Exit code: " + exitCode); // 0 表示正常退出读取标准输出
ProcessBuilder pb = new ProcessBuilder("java", "-version");
pb.redirectErrorStream(true); // 将 stderr 合并到 stdout
Process process = pb.start();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
process.waitFor();分离读取 stdout 和 stderr
ProcessBuilder pb = new ProcessBuilder("mvn", "clean", "package");
Process process = pb.start();
// 必须异步消费,否则缓冲区满会导致进程阻塞(死锁)
Thread stdoutThread = new Thread(() -> {
try (BufferedReader r = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
r.lines().forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
});
Thread stderrThread = new Thread(() -> {
try (BufferedReader r = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
r.lines().forEach(System.err::println);
} catch (IOException e) {
e.printStackTrace();
}
});
stdoutThread.start();
stderrThread.start();
int exitCode = process.waitFor();
stdoutThread.join();
stderrThread.join();配置选项
工作目录
ProcessBuilder pb = new ProcessBuilder("npm", "install");
pb.directory(new File("/path/to/project"));环境变量
ProcessBuilder pb = new ProcessBuilder("python3", "script.py");
Map<String, String> env = pb.environment(); // 继承自 JVM 进程的环境变量
// 读取
System.out.println(env.get("PATH"));
// 添加 / 覆盖
env.put("MY_VAR", "hello");
env.put("JAVA_HOME", "/usr/lib/jvm/java-17");
// 删除
env.remove("UNNECESSARY_VAR");链式配置(Java 9+)
Process p = new ProcessBuilder("echo", "hello")
.directory(new File("/tmp"))
.redirectErrorStream(true)
.start();I/O 重定向
ProcessBuilder.Redirect 提供多种重定向策略:
| 常量 / 方法 | 说明 |
|---|---|
Redirect.PIPE | 默认值,通过流读写 |
Redirect.INHERIT | 继承父进程(JVM)的 I/O |
Redirect.DISCARD | 丢弃输出(Java 9+) |
Redirect.to(file) | 输出写入文件(覆盖) |
Redirect.appendTo(file) | 输出追加到文件 |
Redirect.from(file) | 从文件读取输入 |
File logFile = new File("output.log");
File inputFile = new File("input.txt");
ProcessBuilder pb = new ProcessBuilder("my-script.sh")
.redirectInput(inputFile) // stdin 来自文件
.redirectOutput(Redirect.appendTo(logFile)) // stdout 追加到日志
.redirectError(Redirect.INHERIT); // stderr 打印到控制台
pb.start().waitFor();合并 stderr 到 stdout
pb.redirectErrorStream(true); // stderr 合并进 getInputStream()Process 对象
Process p = pb.start();
// I/O 流
InputStream stdout = p.getInputStream(); // 读进程输出
InputStream stderr = p.getErrorStream(); // 读进程错误
OutputStream stdin = p.getOutputStream(); // 向进程写输入
// 向 stdin 写数据
try (PrintWriter writer = new PrintWriter(
new BufferedWriter(new OutputStreamWriter(p.getOutputStream())))) {
writer.println("some input");
writer.flush();
}
// 等待
p.waitFor(); // 阻塞直到结束
p.waitFor(10, TimeUnit.SECONDS); // 超时等待,返回是否在超时内结束
// 状态
p.exitValue(); // 退出码(进程未结束时抛 IllegalThreadStateException)
p.isAlive(); // 是否仍在运行
// 终止
p.destroy(); // 请求终止(SIGTERM)
p.destroyForcibly(); // 强制终止(SIGKILL)
// Java 9+ 进程信息
ProcessHandle handle = p.toHandle();
handle.pid(); // 进程 PID
handle.info().command(); // 可执行文件路径(Optional)
handle.info().startInstant(); // 启动时间(Optional)超时控制与异常处理
ProcessBuilder pb = new ProcessBuilder("long-running-task");
Process process = pb.start();
boolean finished = process.waitFor(30, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
throw new RuntimeException("Process timed out after 30s");
}
int exitCode = process.exitValue();
if (exitCode != 0) {
throw new RuntimeException("Process failed with exit code: " + exitCode);
}封装工具示例
public static String exec(String... command) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
Process process = pb.start();
String output;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
output = reader.lines().collect(Collectors.joining("\n"));
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Command failed (" + exitCode + "): " + output);
}
return output;
}
// 使用
String log = exec("git", "log", "--oneline", "-5");
System.out.println(log);ProcessBuilder vs Runtime.exec()
ProcessBuilder | Runtime.exec() | |
|---|---|---|
| 命令传入 | List / 可变参数,安全 | 字符串形式有命令注入风险 |
| 环境变量 | environment() 直接修改 Map | 需通过参数传数组 |
| 工作目录 | directory() 方法 | 通过参数传 File |
| I/O 重定向 | 内置 Redirect API | 不支持,只能手动读写流 |
| 推荐程度 | ✅ 推荐 | 不推荐(旧 API) |
注意事项
- 必须消费输出流:进程输出大量数据时,若不读取则缓冲区满,进程阻塞,导致
waitFor()永久挂起。 - 命令必须分词传入:
new ProcessBuilder("ls -la")是错误的,应写new ProcessBuilder("ls", "-la")。 - Shell 语法需显式调用 Shell:管道、通配符等 Shell 特性需通过
sh -c传入。
// Linux / macOS
new ProcessBuilder("sh", "-c", "ls -la | grep .java").start();
// Windows
new ProcessBuilder("cmd.exe", "/c", "dir /b *.java").start();