添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

1. 概述

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

在本教程中, 我们将看看 Java 如何使用 ProcessBuilder API 缓解这种情况。

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. 示例

    现在我们已经对 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>
  • 这一次,我们没有使用构造函数,而是通过 命令(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) 将工作目录设置为项目的 src dir。然后,我们运行一个简单的目录列表命令,并检查输出是否包含子目录 main test

    3.4. 重定向标准输入和输出

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

    默认情况下,我们的进程从管道读取输入。我们可以通过 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中对 ProcessBuilder API进行了哪些添加。

    4. Java 9 新增内容

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

    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 中引入了哪些新增功能。