Quellcode durchsuchen

教程:整理 doc.md 记录 JAVA Process 使用要点

yangyi vor 1 Woche
Ursprung
Commit
077abd2758
3 geänderte Dateien mit 180 neuen und 0 gelöschten Zeilen
  1. 28 0
      AGENTS.md
  2. 151 0
      doc.md
  3. 1 0
      process.md

+ 28 - 0
AGENTS.md

@@ -0,0 +1,28 @@
+# AGENTS.md
+
+Maven single-module demo (target Java 17) teaching `ProcessBuilder`/`Process`. No tests, no lint, no `exec-maven-plugin`; the only dependency is `logback-classic` (slf4j). Code comments and tutorial notes (`process.md`) are in Chinese.
+
+## Running examples
+Classes are plain `public static void main` entry points in `src/main/java/space/anyi/process/`. There is no run target, so run manually after compiling:
+
+```bash
+mvn -q compile
+java -cp "target/classes:$(mvn -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout | tail -1)" space.anyi.process.<ClassName>
+```
+
+All examples declare `static Logger log`, so slf4j jars must be on the classpath or you get `NoClassDefFoundError: org/slf4j/LoggerFactory` (when manually invoking `java -cp target/classes`, not an env problem).
+
+## Child-process fixtures
+- Per-topic fixture dirs under `src/main/resources/code/`: `hello/` (prints greeting), `io/` (echoes stdin, writes stderr), `executeStatus/` (sleeps 10s). Each demo's `workDir` points at exactly one fixture dir.
+- Demos invoke `javac`/`java` at runtime to compile and run fixture `.java` files; shared helpers live in `space.anyi.process.Jdk` (`javac()`/`java()` return absolute paths from `java.home`, `compileFixture()` runs javac). Spawned processes' PATH often lacks the JDK bin dir (`error=2`) — never go back to bare `javac`/`java`.
+- Demos hardcode `workDir` relative to `user.dir` → run them from the repo root, or `ProcessBuilder.start()` throws `IOException: error=2`. Keep `workDir` in sync with the fixture tree when moving files.
+- Also: `space.anyi.process.Main` in package root must NOT exist — `Jdk.compileFixture` uses `-encoding UTF-8` + `Main` and fixtures are in the default package.
+
+## Working rules
+- Every example's spawned child process must run the Java program under `src/main/resources/code/`, never an ad-hoc script or system command. Keep the fixture tree in sync with each example's `workDir`.
+- Every code example must carry reasonable comments (Chinese, matching repo style) explaining what each step does and why.
+- Commit to the local git repo after completing each example/point (`git add` + `git commit`); commit in small increments, one point per commit.
+
+## Known API traps in these examples
+- `new BufferedReader(new InputStreamReader(p.getInputStream())).lines().forEach(...)` **blocks the calling thread until the child exits** (stdout EOF only closes on exit). Draining stdout before `waitFor(timeout)` pins the main thread for the full child runtime (e.g. 10s) and silently defeats the timeout semantics. Drain child output on a background thread when using a timed `waitFor`.
+- `Process.waitFor(timeout, unit)` returns a boolean, not an exit code. After a timeout it returns `false` with the child still alive; calling `process.exitValue()` there throws `IllegalThreadStateException` — `destroy()` + blocking `waitFor()` before reading `exitValue()`.

+ 151 - 0
doc.md

@@ -0,0 +1,151 @@
+# JAVA Process 使用教程
+
+本教程通过 3 个可运行的示例,讲解 `ProcessBuilder` / `Process` 的核心用法。所有示例都以
+`src/main/resources/code/` 下的 `.java` 程序作为子进程,先编译再运行。
+
+## 运行示例
+
+```bash
+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`。
+
+---
+
+## 一、通过 ProcessBuilder 构建 Process —— `QuickStart`
+
+入门示例:`space.anyi.process.QuickStart`
+
+```java
+// 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`。
+
+---
+
+## 二、获取 Process 的输入和输出 —— `ProcessIOExample`
+
+示例:`space.anyi.process.ProcessIOExample`
+
+子进程(`code/io/Main.java`)从标准输入逐行读取、回显到标准输出,读到 EOF 后向标准错误打印统计信息。主进程演示:
+
+```java
+// 标准输出、标准错误分别在后台线程读取,避免阻塞主线程、也避免管道写满导致子进程卡死
+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` 之前这样读,
+  会把主线程钉住整个子进程生命周期,静默破坏超时语义(本仓库的踩坑点)。
+- 若不读子进程输出且输出量超过管道缓冲区(Linux 约 64KB),子进程会因管道写满而阻塞、永不退出。
+  后台异步读取 + 超时后 `destroy()` 的设计可以兜住这种情况。
+- 想合并 stdout/stderr 时调用 `processBuilder.redirectErrorStream(true)`,两条流合到 `getInputStream()`。
+
+---
+
+## 三、获取 Process 执行的状态码 —— `ExecuteStatusExample`
+
+示例:`space.anyi.process.ExecuteStatusExample`
+
+子进程(`code/executeStatus/Main.java`)睡眠 10 秒后退出,用于对比两种获取退出码的方式:
+
+```java
+// 方式一:阻塞式,一直等到子进程结束,返回退出码(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。
+
+---
+
+## 四、Process 的核心对象及专属 API 详解
+
+### ProcessBuilder
+
+作用:**描述“如何启动一个进程”**,配置完成后调用 `start()` 生成 `Process`。
+
+| 方法 | 参数 | 返回 | 说明 |
+|---|---|---|---|
+| `ProcessBuilder(List<String> command)` | 命令及参数的字符串列表 | — | 构建器 |
+| `ProcessBuilder(List<String>)` / `List<String> command()` 变体 | 命令列表/单个命令+参数数组 | 构建器自身 | 设置或读取命令 |
+| `directory(File dir)` | 子进程工作目录 | 构建器自身 | 不设置则继承父进程目录 |
+| `environment()` | 无 | `Map<String,String>` | 返回可变的子进程环境变量视图(增删改影响子进程) |
+| `redirectInput/Output/Error(File)` | 文件 | 构建器自身 | 把子进程标准流重定向到文件 |
+| `redirectErrorStream(boolean)` | 是否合并 stderr 到 stdout | 构建器自身 | `true` 时子进程 stderr 写入 stdout 管道 |
+| `start()` | 无 | `Process` | 启动子进程;目录不存在/命令找不到抛 `IOException` |
+
+> 环境变量示例见 `ProcessBuilderExample`:`processBuilder.environment()` 与 `System.getenv()`
+> 内容一致,向返回的 Map 里 `put` 即可为子进程注入环境变量。
+
+### Process
+
+作用:**表示一个已启动的子进程**,负责等待结束、读取退出码、与进程交互、销毁进程。
+
+| 方法 | 参数 | 返回 | 说明 |
+|---|---|---|---|
+| `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>` | 进程结束时自动完成 |
+
+---
+
+## 常见踩坑小结
+
+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()`。

+ 1 - 0
process.md

@@ -6,5 +6,6 @@
    - 错误输出
 3. 获取Process执行的状态码
 4. Process的核心对象及对应API详解
+   > 详细解释API的每个参数和返回值
    - ProcessBuilder
    - Process