本教程通过 5 个可运行的示例,讲解 ProcessBuilder / Process 的核心用法。所有示例都以
src/main/resources/code/ 下的 .java 程序作为子进程,先编译再运行。
每个案例点由独立的 static method 演示,在 main() 中依次调用。
mvn -q compile
java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout | tail -1)" space.anyi.process.<类名>
各示例声明了 static Logger(slf4j),手工执行时 classpath 必须带上 logback/slf4j 依赖,
否则报 NoClassDefFoundError: org/slf4j/LoggerFactory。
QuickStart案例点:buildProcess()(示例类:space.anyi.process.QuickStart)
// 1. 构建命令并启动子进程:可执行文件绝对路径 + Main 类名,directory() 指定工作目录
Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
.directory(workDir)
.start();
// 2. 读取子进程的标准输出
new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
.lines()
.forEach(line -> log.info("子进程标准输出: {}", line));
// 3. 等待子进程结束,返回退出状态码
int exitStatus = process.waitFor();
要点:
start() 在子进程中执行该命令;可执行文件用
System.getProperty("java.home") 拼出的绝对路径(子进程的 PATH 常常没有 JDK 的 bin 目录,裸写 javac/java 会报 error=2)。Jdk.compileFixture(workDir) 会用绝对路径的 javac 编译工作目录下的 Main.java。ProcessIOExample案例点:standardOutput() / standardError() / standardInput()(示例类:space.anyi.process.ProcessIOExample)
子进程(code/io/Main.java)启动时打印一行 stdout,从标准输入逐行读取并回显,读到 EOF 后向标准错误打印统计信息。三个案例点:
standardOutput():启动子进程、后台读 getInputStream(),看到启动横幅即标准输出 ✓standardError():关闭 stdin 后读 getErrorStream(),看到 EOF 统计行即错误输出 ✓standardInput():向 getOutputStream() 写两行,子进程回显到 stdout ✓
// 标准输出、标准错误分别在后台线程读取,避免阻塞主线程、也避免管道写满导致子进程卡死
Thread stdoutDrain = drain("stdout", process.getInputStream());
Thread stderrDrain = drain("stderr", process.getErrorStream());
// 向子进程标准输入写入数据;关闭输入流即表示 EOF
try (OutputStream stdin = process.getOutputStream()) {
stdin.write("第一行输入\r\n".getBytes(StandardCharsets.UTF_8));
stdin.write("第二行输入\r\n".getBytes(StandardCharsets.UTF_8));
}
对应关系:
| 子进程侧 | 主进程侧 API | 说明 |
|---|---|---|
| 标准输入 (stdin) | process.getOutputStream() |
主进程向该流写入,即子进程的输入 |
| 标准输出 (stdout) | process.getInputStream() |
子进程 println 的内容 |
| 标准错误 (stderr) | process.getErrorStream() |
子进程 err.println 的内容 |
要点:
BufferedReader(...).lines().forEach(...) 是同步阻塞的,
会一直读到 EOF 为止,而 EOF 只有在子进程退出时才出现——在限时 waitFor 之前这样读,
会把主线程钉住整个子进程生命周期,静默破坏超时语义(本仓库的踩坑点)。destroy() 的设计可以兜住这种情况。ProcessRedirectExample案例点:errorStreamMerged() / redirectToFile() / redirectEnum()(示例类:space.anyi.process.ProcessRedirectExample)
子进程复用 code/io/Main.java。三个案例点:
errorStreamMerged()// stderr 合并写入 stdout 管道,合并后的整体从 getInputStream() 读取,不再有独立 stderr 流
Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
.directory(WORK_DIR)
.redirectErrorStream(true)
.start();
演示结果里 child: started...(stdout)与 child: stdin EOF...(stderr)都出现在同一路 [merged] 日志中。
redirectToFile().redirectInput(input.toFile()) // stdin 从文件读
.redirectOutput(stdout.toFile()) // stdout 写到文件
.redirectError(stderr.toFile()) // stderr 写到文件
子进程结束后读回文件校验:stdout.txt 含两行回显,stderr.txt 含一行 EOF 统计。可用于把子进程输出落盘、日志归档等场景。
redirectEnum()| 枚举值 | 含义 | 演示 |
|---|---|---|
ProcessBuilder.Redirect.PIPE |
默认值,子进程输出进入管道,父进程用 getInputStream() 读取 |
逐行读回子进程输出 |
ProcessBuilder.Redirect.INHERIT |
子进程 stdout/stderr 直接打印到父进程的控制台,不进管道 | 两行内容直接出现在父控制台 |
ProcessBuilder.Redirect.DISCARD |
子进程输出被直接丢弃 | 进程正常退出但读不到任何输出 |
Redirect还提供to(File)(输出重定向到文件,覆盖)与appendTo(File)(追加)两个工厂方法, 可用于redirectInput/Output/Error(Redirect)。
ProcessAsyncExample案例点:asyncByThread() / asyncByCompletableFuture()(示例类:space.anyi.process.ProcessAsyncExample)
asyncByThread()// 每个流一个独立线程读取,主线程不阻塞在读取上,也避免管道写满导致子进程卡死
Thread stdoutDrain = drain("thread-stdout", process.getInputStream());
Thread stderrDrain = drain("thread-stderr", process.getErrorStream());
...
stdoutDrain.join(); // 读到 EOF(子进程退出)后汇合
stderrDrain.join();
asyncByCompletableFuture()// stdout / stderr 读取各封装成一个异步任务
CompletableFuture<Void> stdoutFuture = CompletableFuture.runAsync(() ->
new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
.lines().forEach(line -> log.info("[cf-stdout] {}", line)));
CompletableFuture<Void> stderrFuture = CompletableFuture.runAsync(() -> ...);
// 进程结束时 onExit() 自动完成该 Future,注册回调即可在退出时拿到退出码
CompletableFuture<Void> exitFuture = process.onExit()
.thenAccept(p -> log.info("onExit 回调: 退出码 {}", p.exitValue()));
// 等待所有异步任务结束
CompletableFuture.allOf(stdoutFuture, stderrFuture, exitFuture).join();
要点:process.onExit() 返回 CompletableFuture<Process>,不依赖 destroy()/阻塞读取,
适合与函数式回调、并发编排(allOf/thenAccept)结合。
ExecuteStatusExample案例点:blockingWaitFor() / timedWaitFor()(示例类:space.anyi.process.ExecuteStatusExample)
子进程(code/executeStatus/Main.java)睡眠 10 秒后退出,用来对比两种获取退出码的方式:
// 方式一:阻塞式,一直等到子进程结束,返回退出码(0 正常,非 0 异常)
int exitValue = process.waitFor();
// 方式二:限时等待,返回 boolean(超时时间内是否退出),不是退出码
boolean flag = process.waitFor(2, TimeUnit.SECONDS);
if (!flag) {
process.destroy(); // 超时后子进程仍存活,先发终止信号
process.waitFor(); // 再阻塞等待它真正结束
}
int exitCode = process.exitValue(); // 被信号终止时通常为 143(SIGTERM)
要点:
waitFor() 无参:返回 int 退出码。waitFor(long timeout, TimeUnit unit):返回 boolean。超时返回 false 时子进程仍然存活,
此时调用 process.exitValue() 会抛 IllegalThreadStateException;必须先 destroy() + 阻塞
waitFor() 才能读取退出码。process.destroy() 在 Linux 上发送 SIGTERM,属于优雅终止;可用 destroyForcibly() 直接 SIGKILL。作用:描述“如何启动一个进程”,配置完成后调用 start() 生成 Process。
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
ProcessBuilder(List<String> command) |
命令及参数的字符串列表 | — | 构建器 |
ProcessBuilder(List<String>) / List<String> command() 变体 |
命令列表/单个命令+参数数组 | 构建器自身 | 设置或读取命令 |
directory(File dir) |
子进程工作目录 | 构建器自身 | 不设置则继承父进程目录 |
environment() |
无 | Map<String,String> |
返回可变的子进程环境变量视图(增删改影响子进程) |
redirectInput/Output/Error(File) |
文件 | 构建器自身 | 把子进程标准流重定向到文件 |
redirectInput/Output/Error(Redirect) |
Redirect 枚举 |
构建器自身 | PIPE/INHERIT/DISCARD/to(file)/appendTo(file) |
redirectErrorStream(boolean) |
是否合并 stderr 到 stdout | 构建器自身 | true 时子进程 stderr 写入 stdout 管道 |
start() |
无 | Process |
启动子进程;目录不存在/命令找不到抛 IOException |
环境变量示例见
ProcessBuilderExample:processBuilder.environment()与System.getenv()内容一致,向返回的 Map 里put即可为子进程注入环境变量。
作用:表示一个已启动的子进程,负责等待结束、读取退出码、与进程交互、销毁进程。
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
getOutputStream() |
无 | OutputStream |
子进程的标准输入 |
getInputStream() |
无 | InputStream |
子进程的标准输出 |
getErrorStream() |
无 | InputStream |
子进程的标准错误 |
waitFor() |
无 | int |
阻塞直到子进程结束,返回退出码 |
waitFor(long, TimeUnit) |
等待时长、单位 | boolean |
限时等待;超时未退出返回 false |
exitValue() |
无 | int |
退出码;进程未结束调用抛 IllegalThreadStateException |
destroy() |
无 | 无 | 终止进程(Linux 为 SIGTERM,优雅) |
destroyForcibly() |
无 | Process |
强制终止(Linux 为 SIGKILL) |
isAlive() |
无 | boolean |
进程是否仍在运行 |
onExit() |
无 | CompletableFuture<Process> |
进程结束时自动完成 |
javac、java 用 java.home 的绝对路径,不要裸写命令名。user.dir 相对的工作目录要跟 src/main/resources/code/ 下的夹具目录保持一致,否则 error=2。lines().forEach(...) 读子进程输出会阻塞到子进程退出,限时 waitFor 前要在后台线程读取。waitFor(timeout, unit) 返回 boolean;超时后取退出码要 destroy() + waitFor() 再 exitValue()。