doc.md 7.5 KB

JAVA Process 使用教程

本教程通过 3 个可运行的示例,讲解 ProcessBuilder / Process 的核心用法。所有示例都以 src/main/resources/code/ 下的 .java 程序作为子进程,先编译再运行。

运行示例

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

// 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 后向标准错误打印统计信息。主进程演示:

// 标准输出、标准错误分别在后台线程读取,避免阻塞主线程、也避免管道写满导致子进程卡死
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 秒后退出,用于对比两种获取退出码的方式:

// 方式一:阻塞式,一直等到子进程结束,返回退出码(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

环境变量示例见 ProcessBuilderExampleprocessBuilder.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. 编译/运行子进程的 javacjavajava.home绝对路径,不要裸写命令名。
  2. user.dir 相对的工作目录要跟 src/main/resources/code/ 下的夹具目录保持一致,否则 error=2
  3. 同步 lines().forEach(...) 读子进程输出会阻塞到子进程退出,限时 waitFor 前要在后台线程读取。
  4. waitFor(timeout, unit) 返回 boolean;超时后取退出码要 destroy() + waitFor()exitValue()