|
|
@@ -1,6 +1,6 @@
|
|
|
# JAVA Process 使用教程
|
|
|
|
|
|
-本教程通过 5 个可运行的示例,讲解 `ProcessBuilder` / `Process` 的核心用法。所有示例都以
|
|
|
+本教程通过 6 个可运行的示例,讲解 `ProcessBuilder` / `Process` 的核心用法。所有示例都以
|
|
|
`src/main/resources/code/` 下的 `.java` 程序作为子进程,先编译再运行。
|
|
|
每个**案例点**由独立的 `static` method 演示,在 `main()` 中依次调用。
|
|
|
|
|
|
@@ -13,6 +13,28 @@ java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/
|
|
|
|
|
|
各示例声明了 `static Logger`(slf4j),手工执行时 classpath 必须带上 logback/slf4j 依赖,
|
|
|
否则报 `NoClassDefFoundError: org/slf4j/LoggerFactory`。
|
|
|
+示例的行为由 JUnit 5 测试守护:`mvn test` 直接驱动夹具子进程做端到端校验。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 零、概念铺垫(30 秒建立心智模型)
|
|
|
+
|
|
|
+- **进程(Process)**:操作系统里一个正在运行的程序实例。Java 代码可以启动一个外部程序(如 `java`、
|
|
|
+ `javac`,或任意可执行文件)作为**子进程**,用一个逻辑句柄来操控它。
|
|
|
+- **ProcessBuilder**:**"如何启动"** 的配置器。它只管描述命令、工作目录、环境变量、三路标准流走哪个
|
|
|
+ 源头;配置完成后调用 `start()` 才真正启动子进程,并返回一个 `Process` 句柄。
|
|
|
+- **Process**:**"已启动的子进程"** 的句柄。用它可以等待结束、读退出码、write/read 三路标准流、销毁进程。
|
|
|
+- 主进程与子进程通过**三路管道**交互,方向最容易搞反,记住这句:
|
|
|
+
|
|
|
+```
|
|
|
+ 主进程 子进程
|
|
|
+ getOutputStream() ────────────▶ 标准输入 stdin
|
|
|
+ getInputStream() ◀─────────── 标准输出 stdout
|
|
|
+ getErrorStream() ◀─────────── 标准错误 stderr
|
|
|
+```
|
|
|
+
|
|
|
+> 子进程程序固定放在 `src/main/resources/code/<主题>/Main.java`,示例先编译再运行,
|
|
|
+> 公共类 `Jdk`(`javac()`/`java()` 绝对路径、`compileFixture()`)负责这两件事。
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -40,6 +62,22 @@ int exitStatus = process.waitFor();
|
|
|
`System.getProperty("java.home")` 拼出的**绝对路径**(子进程的 PATH 常常没有 JDK 的 bin 目录,裸写 `javac`/`java` 会报 `error=2`)。
|
|
|
- 必须先编译子进程程序:公共类 `Jdk.compileFixture(workDir)` 会用绝对路径的 `javac` 编译工作目录下的 `Main.java`。
|
|
|
|
|
|
+预期输出:
|
|
|
+
|
|
|
+```
|
|
|
+子进程标准输出: Hello from child process!
|
|
|
+子进程标准输出: child pid = 12345
|
|
|
+子进程退出状态码: 0
|
|
|
+```
|
|
|
+
|
|
|
+> **警告:ProcessBuilder 不经过 shell。** 列表里每一项就是子进程收到的**一个参数**:
|
|
|
+> - 不要整个命令写成一个带空格的字符串,如 `"java -version"`(会被当作一个叫 `java -version` 的可执行文件);
|
|
|
+> - 不要期待引号展开、`$VAR`、通配符、`|` 管道、重定向等 shell 语法生效;
|
|
|
+> - 确实需要 shell 能力时,显式走 `sh`/`cmd`:`List.of("sh", "-c", "echo $HOME | wc -c")`。
|
|
|
+>
|
|
|
+> 平台差异:Windows 下 `javac`/`java` 的文件名带 `.exe`,目录分隔符是 `\`,
|
|
|
+> `process.destroy()` 走 `TerminateProcess`(不产生 143 这种退出码)。
|
|
|
+
|
|
|
---
|
|
|
|
|
|
## 二、获取 Process 的输入和输出 —— `ProcessIOExample`
|
|
|
@@ -79,9 +117,22 @@ try (OutputStream stdin = process.getOutputStream()) {
|
|
|
- 若不读子进程输出且输出量超过管道缓冲区(Linux 约 64KB),子进程会因管道写满而阻塞、永不退出。
|
|
|
后台异步读取 + 超时后 `destroy()` 的设计可以兜住这种情况。
|
|
|
|
|
|
+预期输出(线程日志与主线程日志顺序可能交错):
|
|
|
+
|
|
|
+```
|
|
|
+[stdout] child: started, waiting for stdin
|
|
|
+[stdout] child: echo 第一行输入
|
|
|
+[stdout] child: echo 第二行输入
|
|
|
+[stderr] child: stdin EOF after 2 line(s)
|
|
|
+子进程是否已退出: true
|
|
|
+退出状态码: 0
|
|
|
+```
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## 二-1、标准输出与错误输出的重定向 —— `ProcessRedirectExample`
|
|
|
+## 二-1、标准输出与错误输出的重定向 —— `ProcessRedirectExample`(进阶)
|
|
|
+
|
|
|
+> **进阶章节**:掌握前几节后再看。重定向用于"子进程输出不进管道"的常见场景。
|
|
|
|
|
|
案例点:`errorStreamMerged()` / `redirectToFile()` / `redirectEnum()`(示例类:`space.anyi.process.ProcessRedirectExample`)
|
|
|
|
|
|
@@ -96,7 +147,13 @@ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main"
|
|
|
.redirectErrorStream(true)
|
|
|
.start();
|
|
|
```
|
|
|
-演示结果里 `child: started...`(stdout)与 `child: stdin EOF...`(stderr)都出现在同一路 `[merged]` 日志中。
|
|
|
+演示结果里 `child: started...`(stdout)与 `child: stdin EOF...`(stderr)都出现在同一路 `[merged]` 日志中:
|
|
|
+
|
|
|
+```
|
|
|
+[merged] child: started, waiting for stdin
|
|
|
+[merged] child: stdin EOF after 0 line(s)
|
|
|
+退出码: 0
|
|
|
+```
|
|
|
|
|
|
### 重定向到文件 —— `redirectToFile()`
|
|
|
|
|
|
@@ -105,7 +162,14 @@ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main"
|
|
|
.redirectOutput(stdout.toFile()) // stdout 写到文件
|
|
|
.redirectError(stderr.toFile()) // stderr 写到文件
|
|
|
```
|
|
|
-子进程结束后读回文件校验:`stdout.txt` 含两行回显,`stderr.txt` 含一行 EOF 统计。可用于把子进程输出落盘、日志归档等场景。
|
|
|
+子进程结束后读回文件校验:
|
|
|
+
|
|
|
+```
|
|
|
+退出码: 0
|
|
|
+stdout 文件内容: [child: started, waiting for stdin, child: echo 来自文件的输入行, child: echo 第二行来自文件]
|
|
|
+stderr 文件内容: [child: stdin EOF after 2 line(s)]
|
|
|
+```
|
|
|
+可用于把子进程输出落盘、日志归档等场景。
|
|
|
|
|
|
### Redirect 枚举 —— `redirectEnum()`
|
|
|
|
|
|
@@ -120,7 +184,9 @@ Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main"
|
|
|
|
|
|
---
|
|
|
|
|
|
-## 二-2、异步处理输入和输出 —— `ProcessAsyncExample`
|
|
|
+## 二-2、异步处理输入和输出 —— `ProcessAsyncExample`(进阶)
|
|
|
+
|
|
|
+> **进阶章节**:需要掌握 JDK 8+ 的 `CompletableFuture` 再阅读。
|
|
|
|
|
|
案例点:`asyncByThread()` / `asyncByCompletableFuture()`(示例类:`space.anyi.process.ProcessAsyncExample`)
|
|
|
|
|
|
@@ -155,6 +221,20 @@ CompletableFuture.allOf(stdoutFuture, stderrFuture, exitFuture).join();
|
|
|
要点:`process.onExit()` 返回 `CompletableFuture<Process>`,不依赖 `destroy()`/阻塞读取,
|
|
|
适合与函数式回调、并发编排(`allOf`/`thenAccept`)结合。
|
|
|
|
|
|
+预期输出:
|
|
|
+
|
|
|
+```
|
|
|
+[thread-stdout] child: started, waiting for stdin
|
|
|
+[thread-stdout] child: echo hello thread
|
|
|
+[thread-stderr] child: stdin EOF after 1 line(s)
|
|
|
+退出码: 0
|
|
|
+== CompletableFuture 异步读取 ==
|
|
|
+[cf-stdout] child: started, waiting for stdin
|
|
|
+[cf-stdout] child: echo hello future
|
|
|
+[cf-stderr] child: stdin EOF after 1 line(s)
|
|
|
+onExit 回调: 退出码 0
|
|
|
+```
|
|
|
+
|
|
|
---
|
|
|
|
|
|
## 三、获取 Process 执行的状态码 —— `ExecuteStatusExample`
|
|
|
@@ -183,6 +263,17 @@ int exitCode = process.exitValue(); // 被信号终止时通常为 143(SIGTER
|
|
|
`waitFor()` 才能读取退出码。
|
|
|
- `process.destroy()` 在 Linux 上发送 SIGTERM,属于优雅终止;可用 `destroyForcibly()` 直接 SIGKILL。
|
|
|
|
|
|
+预期输出:
|
|
|
+
|
|
|
+```
|
|
|
+== 方式一:waitFor() 阻塞等待 ==
|
|
|
+阻塞等待退出码: 0 # 大约 10 秒后出现(子进程睡眠 10 秒)
|
|
|
+== 方式二:waitFor(2, SECONDS) 限时等待 ==
|
|
|
+2 秒内是否退出: false
|
|
|
+超时未退出,destroy 销毁子进程后阻塞等待
|
|
|
+销毁后退出码: 143 # SIGTERM 信号终止的退出码
|
|
|
+```
|
|
|
+
|
|
|
---
|
|
|
|
|
|
## 四、Process 的核心对象及专属 API 详解
|
|
|
@@ -224,9 +315,54 @@ int exitCode = process.exitValue(); // 被信号终止时通常为 143(SIGTER
|
|
|
|
|
|
---
|
|
|
|
|
|
+## 五、综合运用:管道式调用外部工具 —— `ProcessPipelineExample`
|
|
|
+
|
|
|
+案例点:`runPipeline()`(示例类:`space.anyi.process.ProcessPipelineExample`)
|
|
|
+
|
|
|
+把子进程当作一条**处理管道**,完整走一遍真实调用的流程:启动 → 异步采集输出 → 喂入数据 → 取回结果 → 校验。
|
|
|
+
|
|
|
+```java
|
|
|
+// 1. 启动子进程(外部工具)
|
|
|
+Process process = new ProcessBuilder(Jdk.java(), "-Dfile.encoding=UTF-8", "Main")
|
|
|
+ .directory(WORK_DIR)
|
|
|
+ .start();
|
|
|
+
|
|
|
+// 2. 异步把 stdout 收集成 List<String>、异步消费 stderr
|
|
|
+CompletableFuture<List<String>> resultFuture = CompletableFuture.supplyAsync(() ->
|
|
|
+ new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
|
|
|
+ .lines().collect(Collectors.toList()));
|
|
|
+CompletableFuture<Void> stderrDrain = CompletableFuture.runAsync(() -> ...);
|
|
|
+
|
|
|
+// 3. 向子进程 stdin 喂入数据,关闭输入流表示 EOF
|
|
|
+try (OutputStream stdin = process.getOutputStream()) {
|
|
|
+ for (int i = 1; i <= 3; i++) writeLine(stdin, "待处理数据-" + i);
|
|
|
+}
|
|
|
+
|
|
|
+// 4. 等待退出码,取回处理结果并校验行数
|
|
|
+int exit = process.waitFor();
|
|
|
+List<String> result = resultFuture.get();
|
|
|
+long echoed = result.stream().filter(line -> line.startsWith("child: echo")).count();
|
|
|
+```
|
|
|
+
|
|
|
+预期输出:
|
|
|
+
|
|
|
+```
|
|
|
+[stderr] child: stdin EOF after 3 line(s)
|
|
|
+退出码: 0
|
|
|
+回显结果: [child: started, waiting for stdin, child: echo 待处理数据-1, child: echo 待处理数据-2, child: echo 待处理数据-3]
|
|
|
+回显行数: 3,符合预期: true
|
|
|
+```
|
|
|
+
|
|
|
+要点:这一段把前面四章的知识串了起来——`ProcessBuilder` 构建(一)、三路管道(二)、
|
|
|
+异步读取 stdout/stderr(二-2)、`waitFor()` 拿退出码(三)。实际项目中把第 2 步的"异步收集
|
|
|
+stdout"换成业务处理(如逐行解析、透传到日志),子进程就成了你程序里的一级生产/消费管道。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
## 常见踩坑小结
|
|
|
|
|
|
1. 编译/运行子进程的 `javac`、`java` 用 `java.home` 的**绝对路径**,不要裸写命令名。
|
|
|
2. `user.dir` 相对的工作目录要跟 `src/main/resources/code/` 下的夹具目录保持一致,否则 `error=2`。
|
|
|
3. 同步 `lines().forEach(...)` 读子进程输出会阻塞到子进程退出,限时 `waitFor` 前要在后台线程读取。
|
|
|
-4. `waitFor(timeout, unit)` 返回 boolean;超时后取退出码要 `destroy() + waitFor()` 再 `exitValue()`。
|
|
|
+4. `waitFor(timeout, unit)` 返回 boolean;超时后取退出码要 `destroy() + waitFor()` 再 `exitValue()`。
|
|
|
+5. `ProcessBuilder` 不经 shell:参数数组每一项就是一个参数,引号/`$VAR`/通配符/`|` 都不生效,需要时用 `sh -c "..."`。
|