Java进程的创建和运行(附带实例)
如果你已经了解了如何在同一程序的不同线程中执行 Java 代码,有时还需要执行另一个程序。为此,可以使用 ProcessBuilder 和 Process 类:
注意,ProcessBuilder 类是对 Runtime.exec 调用的一种更灵活的替换形式。
每个进程都有一个工作目录,用于解析相对目录名。默认情况下,进程具有与虚拟机相同的工作目录,通常是启动 java 程序的目录。可以使用 directory() 方法改变工作目录:
注意,用于配置 ProcessBuilder 的每个方法都会返回其自身,以便你可以把命令串起来。最终,将调用以下方法:
接下来,要指定如何处理进程的标准输入、输出和错误流。默认情况下,它们中的每一个都是一个管道,可以使用以下方法访问:
可以指定新进程的输入、输出和错误流与 JVM 的这 3 个流相同。如果用户在控制台运行 JVM,则所有用户输入都会转发到进程,而进程的输出将会显示在控制台上。
调用以下方法可以对所有的 3 个流进行这样的设置:
如果只想继承其中某些流,可以将以下的值传入 redirectInput()、redirectOutput() 或者 redirectError() 方法:
通过提供 File 对象,可以将进程流重定向到文件:
进程启动时,将创建或删除用于输出和错误文件。要追加到现有文件,可以使用:
合并输出和错误流通常很有用,这样就可以按进程生成输出和错误消息的顺序查看它们。调用来启用合并:
最后,你可能还想修改流程的环境变量。这里,构建器的串链语法就不能用了。需要获取构建器的环境(由运行 JVM 的进程的环境变量初始化),然后加入或删除环境变量条目。
下面是一个示例,列举目录树中的唯一扩展:
例如:
要等待进程完成,可以调用:
或者,如果不想无限期地等待,可以这样做:
不必等待进程结束,可以让它继续运行,偶尔调用 isAlive 来查看它是否还处于活动状态。要终止进程,可以调用 destroy() 或 destroyForcibly()。这两个调用之间的区别取决于平台。在 Unix 上,前者使用 SIGTERM 终止进程,后者使用 SIGKILL 终止进程。(如果 destroy() 方法可以正常终止进程,supports NormalTermination 方法返回 true。)
最后,会在流程完成时收到异步通知。调用 process.onExit() 会生成一个 CompletableFuture<Process>,可以使用它来调度任何动作:
- Process 类在单独的操作系统进程中执行命令,并允许你与标准输入、输出和错误流进行交互;
- ProcessBuilder 类允许你配置 Process 对象。
注意,ProcessBuilder 类是对 Runtime.exec 调用的一种更灵活的替换形式。
Java创建进程
首先指定想要执行的命令。可以提供 List<String>,或者直接提供组成命令的字符串:var builder = new ProcessBuilder("gcc", "myapp.c");第一个字符串必须是可执行命令,不是一个 shell 内置命令。例如,要在 Windows 中运行 dir 命令,需要使用字符串"cmd.exe"、"/C"和"dir"创建进程。
每个进程都有一个工作目录,用于解析相对目录名。默认情况下,进程具有与虚拟机相同的工作目录,通常是启动 java 程序的目录。可以使用 directory() 方法改变工作目录:
builder = builder.directory(path.toFile());
注意,用于配置 ProcessBuilder 的每个方法都会返回其自身,以便你可以把命令串起来。最终,将调用以下方法:
Process p = new ProcessBuilder(command).directory(file).start();
接下来,要指定如何处理进程的标准输入、输出和错误流。默认情况下,它们中的每一个都是一个管道,可以使用以下方法访问:
OutputStream processIn = p.getOutputStream(); InputStream processOut = p.getInputStream(); InputStream processErr = p.getErrorStream();或用于文本输入和输出:
BufferedWriter processIn = p.outputWriter(charset); BufferedReader processOut = p.inputReader(charset); BufferedReader processErr = p.errorReader(charset);注意,进程的输入流是 JVM 的输出流!向该流进行写入,所写的任何内容都将成为进程的输入。相反,可以读取进程写入输出和错误流的内容。对你来说,它们都是输入流。
可以指定新进程的输入、输出和错误流与 JVM 的这 3 个流相同。如果用户在控制台运行 JVM,则所有用户输入都会转发到进程,而进程的输出将会显示在控制台上。
调用以下方法可以对所有的 3 个流进行这样的设置:
builder.inheritIO()
如果只想继承其中某些流,可以将以下的值传入 redirectInput()、redirectOutput() 或者 redirectError() 方法:
ProcessBuilder.Redirect.INHERIT例如:
builder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
通过提供 File 对象,可以将进程流重定向到文件:
builder.redirectInput(inputFile) .redirectOutput(outputFile) .redirectError(errorFile)
进程启动时,将创建或删除用于输出和错误文件。要追加到现有文件,可以使用:
builder.redirectOutput(ProcessBuilder.Redirect.appendTo(outputFile));
合并输出和错误流通常很有用,这样就可以按进程生成输出和错误消息的顺序查看它们。调用来启用合并:
builder.redirectErrorStream(true)如果这样做,就不能再在 ProcessBuilder 上调用 redirectError,也不能在 Process 上调用 getErrorStream。
最后,你可能还想修改流程的环境变量。这里,构建器的串链语法就不能用了。需要获取构建器的环境(由运行 JVM 的进程的环境变量初始化),然后加入或删除环境变量条目。
Map<String, String> env = builder.environment(); env.put("LANG", "fr_FR"); env.remove("JAVA_HOME"); Process p = builder.start();如果要利用管道将一个进程的输出作为另一个进程中的输入(就像 shell 中的 | 运算符一样),可以使用 startPipeline() 方法。传入一个进程构造器列表,并读取最后一个进程的结果。
下面是一个示例,列举目录树中的唯一扩展:
List<Process> processes = ProcessBuilder.startPipeline(List.of( new ProcessBuilder("find", "/opt/jdk-17"), new ProcessBuilder("grep", "-o", "\\.[^./]*$"), new ProcessBuilder("sort"), new ProcessBuilder("uniq") )); Process last = processes.get(processes.size() - 1); var result = new String(last.getInputStream().readAllBytes());这只是一个展示机制的例子。当然,对于特定的任务来讲,通过在 Java 中创建目录遍历来解决比运行 4 个进程效率更高。
Java运行进程
配置了构建器后,要调用其 start() 方法启动进程。如果将输入、输出和错误流配置为管道,那么现在可以写入输入流并读取输出和错误。例如:
Process process = new ProcessBuilder("/bin/ls", "-l") .directory(Path.of("/tmp").toFile()) .start(); try (var in = new Scanner(process.getInputStream())) { while (in.hasNextLine()) System.out.println(in.nextLine()); }进程流的缓冲空间是有限的。因此,不能写入太多输入,而且要及时读取输出。如果有大量的输入和输出,可能需要在单独的线程中生产和消费这些输入和输出。
要等待进程完成,可以调用:
int result = process.waitFor();
或者,如果不想无限期地等待,可以这样做:
long delay = ...; if (process.waitfor(delay, TimeUnit.SECONDS)) { int result = process.exitValue(); ... } else { process.destroyForcibly(); }对 waitFor 的第一次调用返回进程的退出值(按照惯例,0 表示成功,或者返回一个非零的错误代码)。如果进程没有超时,则第二个调用返回 true。然后,需要通过调用 exitValue() 方法来获取退出值。
不必等待进程结束,可以让它继续运行,偶尔调用 isAlive 来查看它是否还处于活动状态。要终止进程,可以调用 destroy() 或 destroyForcibly()。这两个调用之间的区别取决于平台。在 Unix 上,前者使用 SIGTERM 终止进程,后者使用 SIGKILL 终止进程。(如果 destroy() 方法可以正常终止进程,supports NormalTermination 方法返回 true。)
最后,会在流程完成时收到异步通知。调用 process.onExit() 会生成一个 CompletableFuture<Process>,可以使用它来调度任何动作:
process.onExit().thenAccept( p -> System.out.println("Exit value: " + p.exitValue()));