java.lang.ProcessBuilder API指南

2025/03/17

1. 概述

Process API提供了一种在Java中执行操作系统命令的强大方法,但是,它有几个选项可能会使其使用起来很麻烦。

在本教程中,我们将了解Java如何使用ProcessBuilder API缓解这一问题

2. ProcessBuilder API

ProcessBuilder类提供创建和配置操作系统进程的方法,每个ProcessBuilder实例允许我们管理一组进程属性,然后我们可以使用这些给定的属性启动一个新的进程。

以下是我们可以使用此API的一些常见场景:

  • 查找当前Java版本
  • 为我们的环境设置自定义键值映射
  • 更改Shell命令运行的工作目录
  • 将输入和输出流重定向到自定义替换
  • 继承当前JVM进程的两个流
  • 从Java代码执行Shell命令

我们将在后面的章节中查看每个内容的实际例子。

但在深入研究工作代码之前,让我们先看看这个API提供了什么样的功能。

2.1 方法摘要

在本节中,我们将回顾一下并简要介绍一下ProcessBuilder类中最重要的方法,这将有助于我们稍后深入研究一些真实的例子:

ProcessBuilder(String... command)

要使用指定的操作系统程序和参数创建一个新的进程构建器,我们可以使用这个方便的构造函数。

directory(File directory)

我们可以通过调用directory方法并传递一个File对象来覆盖当前进程的默认工作目录。默认情况下,当前工作目录设置为user.dir系统属性返回的值

environment()

如果我们想获取当前环境变量,只需调用environment方法即可。它使用System.getenv()返回当前进程环境的副本,但以Map形式返回。

inheritIO()

如果我们想要指定子进程标准I/O的源和目标应该与当前Java进程的相同,我们可以使用inheritIO方法。

redirectInput(File file), redirectOutput(File file), redirectError(File file)

当我们想要将进程生成器的标准输入、输出和错误目标重定向到文件时,我们可以使用这三种类似的重定向方法。

start()

最后但同样重要的一点是,要使用我们配置的内容启动新进程,我们只需调用start()。

我们应该注意,此类不是同步的。例如,如果我们有多个线程同时访问ProcessBuilder实例,则必须在外部管理同步。

3. 示例

现在我们对ProcessBuilder API有了基本的了解,让我们逐步了解一些示例。

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>
  • 这次,我们没有使用构造函数,而是直接通过command(String… command)方法设置命令和参数
  • 然后我们按照前面的例子开始我们的进程

为了完成示例,我们验证输出是否包含我们的问候语:

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);
}

在上面的例子中,我们使用便捷方法directory(File directory)将工作目录设置为项目的src目录,然后我们运行一个简单的目录列表命令,并检查输出是否包含子目录main和test。

3.4 重定向标准输入和输出

在现实世界中,我们可能希望将正在运行的进程的结果捕获到日志文件中,以便进一步分析。幸运的是,ProcessBuilder API内置了对此的支持,正如我们将在此示例中看到的那样。

默认情况下,我们的进程从管道读取输入,我们可以通过Process.getOutputStream()返回的输出流访问此管道

但是,我们很快就会看到,可以使用方法redirectOutput将标准输出重定向到另一个源(例如文件)。在这种情况下,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中ProcessBuilder API添加了哪些内容。

4. Java 9新增功能

Java 9在ProcessBuilder API中引入了管道的概念

public static List<Process> startPipeline(List<ProcessBuilder> builders)

使用startPipeline方法,我们可以传递一个ProcessBuilder对象列表。然后,此静态方法将为每个ProcessBuilder启动一个Process,从而创建一个由其标准输出和标准输入流链接的进程管道。

例如,如果我们要运行如下命令:

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文件,并将结果传送到另一个进程中进行计数

要了解Java 9中对Process API所做的其他改进,请查看我们关于Java 9 Process API改进的文章。

5. 总结

总而言之,在本教程中,我们详细探讨了java.lang.ProcessBuilder API。

首先,我们首先解释了API可以做什么,并总结了最重要的方法。

接下来,我们看了一些实际示例。最后,我们了解了Java 9中API引入了哪些新功能。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章