ProcessBuilder API 指南-Java快速进阶教程
1. 概述
Process API提供了一种在 Java 中执行操作系统命令的强大方法。但是,它有几个选项,可能会使其使用起来很麻烦。
在本教程中,我们将看看 Java 如何使用ProcessBuilderAPI 缓解这种情况。
2.进程生成器接口
类提供了用于创建和配置操作系统进程的方法。每个ProcessBuilder实例都允许我们管理流程属性的集合。然后,我们可以使用这些给定属性启动一个新流程。
以下是我们可以使用此 API 的一些常见场景:
- 查找当前 Java 版本
- 为我们的环境设置自定义键值映射
- 更改运行 shell 命令的工作目录
- 将输入和输出流重定向到自定义替换
- 继承当前 JVM 进程的两个流
- 从 Java 代码执行 shell 命令
我们将在后面的部分中查看每个实例的实际示例。
但在深入研究工作代码之前,让我们看一下此 API 提供了什么样的功能。
2.1. 方法总结
在本节中,我们将退后一步,简要介绍ProcessBuilder类中最重要的方法。当我们稍后深入研究一些真实示例时,这将对我们有所帮助:
-
ProcessBuilder(String... command)
要使用指定的操作系统程序和参数创建新的进程生成器,我们可以使用此方便的构造函数。
-
directory(File directory)
我们可以通过调用directory方法并传递File对象来覆盖当前进程的默认工作目录。默认情况下,当前工作目录设置为user.dir系统属性返回的值。
-
environment()
如果我们想获取当前的环境变量,我们可以简单地调用环境方法。它使用System.getenv() 但作为Map 返回当前进程环境的副本。
-
inheritIO()
如果我们想指定子进程标准 I/O 的源和目标应该与当前 Java 进程的源和目标相同,我们可以使用inheritIO方法。
-
redirectInput(File file), redirectOutput(File file), redirectError(File file)
当我们想要将流程生成器的标准输入、输出和错误目标重定向到文件时,我们可以使用这三种类似的重定向方法。
-
start()
最后但并非最不重要的一点是,要使用我们配置的内容启动一个新进程,我们只需调用start()。
我们应该注意,这个类是不同步的。例如,如果我们有多个线程同时访问ProcessBuilder实例,则必须在外部管理同步。
3. 示例
现在我们已经对ProcessBuilderAPI 有了基本的了解,让我们逐步了解一些示例。
3.1. 使用ProcessBuilder打印 Java 版本
在第一个示例中,我们将使用一个参数运行java命令以获取版本。
Process process = new ProcessBuilder("java", "-version").start();
首先,我们创建ProcessBuilder对象,将命令和参数值传递给构造函数。接下来,我们使用start() 方法启动进程来获取一个 Process对象。
现在让我们看看如何处理输出:
List<String> results = readOutput(process.getInputStream());
assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("java version")));
int exitCode = process.waitFor();
assertEquals("No errors should be detected", 0, exitCode);
在这里,我们正在读取流程输出并验证内容是否符合我们的预期。在最后一步中,我们使用process.waitFor()等待进程完成。
流程完成后,返回值会告诉我们流程是否成功。
要记住的几个要点:
- 参数必须按正确的顺序排列
- 此外,在此示例中,使用了默认的工作目录和环境
- 我们故意在读取输出之前不调用process.waitFor(),因为输出缓冲区可能会使进程停止
- 我们假设java命令可通过PATH变量获得
3.2. 使用修改后的环境启动进程
在下一个示例中,我们将了解如何修改工作环境。
但在我们这样做之前,让我们先看一下我们可以在默认环境中找到的信息类型:
ProcessBuilder processBuilder = new ProcessBuilder();
Map<String, String> environment = processBuilder.environment();
environment.forEach((key, value) -> System.out.println(key + value));
这只会打印出默认提供的每个变量条目:
PATH/usr/bin:/bin:/usr/sbin:/sbin
SHELL/bin/bash
...
现在,我们将向ProcessBuilder对象添加新的环境变量,并运行命令来输出其值:
environment.put("GREETING", "Hola Mundo");
processBuilder.command("/bin/bash", "-c", "echo $GREETING");
Process process = processBuilder.start();
让我们分解这些步骤来了解我们所做的工作:
- 将一个名为“GREETING”的变量(值为“Hola Mundo”)添加到我们的环境中,这是一个标准的Map<String,String>
- 这一次,我们没有使用构造函数,而是通过命令(String...命令)方法直接。
- 然后,我们按照前面的示例开始我们的过程。
为了完成示例,我们验证输出是否包含我们的问候语:
List<String> results = readOutput(process.getInputStream());
assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain java version: ", results, hasItem(containsString("Hola Mundo")));
3.3. 使用修改后的工作目录启动进程
有时更改工作目录可能很有用。在下一个示例中,我们将了解如何做到这一点:
@Test
public void givenProcessBuilder_whenModifyWorkingDir_thenSuccess()
throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "ls");
processBuilder.directory(new File("src"));
Process process = processBuilder.start();
List<String> results = readOutput(process.getInputStream());
assertThat("Results should not be empty", results, is(not(empty())));
assertThat("Results should contain directory listing: ", results, contains("main", "test"));
int exitCode = process.waitFor();
assertEquals("No errors should be detected", 0, exitCode);
}
在上面的例子中,我们使用方便的方法目录(File directory)将工作目录设置为项目的srcdir。然后,我们运行一个简单的目录列表命令,并检查输出是否包含子目录main和test。
3.4. 重定向标准输入和输出
在现实世界中,我们可能希望在日志文件中捕获正在运行的进程的结果以进行进一步分析。幸运的是,ProcessBuilderAPI对此具有内置的支持,正如我们将在本例中看到的那样。
默认情况下,我们的进程从管道读取输入。我们可以通过Process.getOutputStream() 返回的输出流访问此管道。
但是,正如我们稍后将看到的,标准输出可能会使用方法重定向到另一个源,例如文件。在这种情况下,getOutputStream() 将返回一个ProcessBuilder.NullOutputStream。
让我们回到原始示例来打印出 Java 版本。但这次让我们将输出重定向到日志文件而不是标准输出管道:
ProcessBuilder processBuilder = new ProcessBuilder("java", "-version");
processBuilder.redirectErrorStream(true);
File log = folder.newFile("java-version.log");
processBuilder.redirectOutput(log);
Process process = processBuilder.start();
在上面的示例中,我们创建了一个名为 log 的新临时文件,并告诉我们的ProcessBuilder将输出重定向到此文件目标。
在最后一个代码段中,我们只需检查getInputStream() 是否确实为空,并且文件的内容是否符合预期:
assertEquals("If redirected, should be -1 ", -1, process.getInputStream().read());
List<String> lines = Files.lines(log.toPath()).collect(Collectors.toList());
assertThat("Results should contain java version: ", lines, hasItem(containsString("java version")));
现在让我们看一下这个例子的细微变化。例如,当我们希望附加到日志文件而不是每次都创建一个新文件时:
File log = tempFolder.newFile("java-version-append.log");
processBuilder.redirectErrorStream(true);
processBuilder.redirectOutput(Redirect.appendTo(log));
同样重要的是要提到对redirectErrorStream(true)的调用。如果出现任何错误,错误输出将合并到正常进程输出文件中。
当然,我们可以为标准输出和标准错误输出指定单独的文件:
File outputLog = tempFolder.newFile("standard-output.log");
File errorLog = tempFolder.newFile("error.log");
processBuilder.redirectOutput(Redirect.appendTo(outputLog));
processBuilder.redirectError(Redirect.appendTo(errorLog));
3.5. 继承当前进程的 I/O
在这个倒数第二个示例中,我们将看到inheritIO() 方法的实际应用。当我们想要将子进程 I/O 重定向到当前进程的标准 I/O 时,可以使用此方法:
@Test
public void givenProcessBuilder_whenInheritIO_thenSuccess() throws IOException, InterruptedException {
ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", "echo hello");
processBuilder.inheritIO();
Process process = processBuilder.start();
int exitCode = process.waitFor();
assertEquals("No errors should be detected", 0, exitCode);
}
在上面的示例中,通过使用inheritIO() 方法,我们在 IDE 的控制台中看到一个简单的命令的输出。
在下一节中,我们将看看Java 9中对ProcessBuilderAPI进行了哪些添加。
4. Java 9 新增内容
Java 9 在ProcessBuilderAPI 中引入了管道的概念:
public static List<Process> startPipeline(List<ProcessBuilder> builders)
使用startPipeline方法,我们可以传递ProcessBuilder对象的列表。然后,此静态方法将为每个进程生成器启动一个进程。因此,创建一个流程管道,这些流程通过其标准输出和标准输入流链接。
例如,如果我们想运行这样的东西:
find . -name *.java -type f | wc -l
我们要做的是为每个隔离的命令创建一个流程构建器,并将它们组合到一个管道中:
@Test
public void givenProcessBuilder_whenStartingPipeline_thenSuccess()
throws IOException, InterruptedException {
List builders = Arrays.asList(
new ProcessBuilder("find", "src", "-name", "*.java", "-type", "f"),
new ProcessBuilder("wc", "-l"));
List processes = ProcessBuilder.startPipeline(builders);
Process last = processes.get(processes.size() - 1);
List output = readOutput(last.getInputStream());
assertThat("Results should not be empty", output, is(not(empty())));
}
在此示例中,我们将搜索src目录中的所有 java 文件,并将结果管道传输到另一个进程中以对其进行计数。
5. 结论
总而言之,在本教程中,我们详细探讨了java.lang.ProcessBuilderAPI。
首先,我们首先解释了 API 可以做什么,并总结了最重要的方法。
接下来,我们看了一些实际的例子。最后,我们研究了 Java 9 中 API 中引入了哪些新增功能。