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()

ProcessBuilderRuntime.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();

相关链接