java.lang.Process API指南

2025/03/17

1. 简介

在本教程中,我们将深入了解Process API。

为了更深入地了解如何使用Process执行Shell命令,我们可以参考之前的教程

进程指的是一个正在执行的应用程序,Process类提供与这些进程交互的方法,包括提取输出、执行输入、监视生命周期、检查退出状态以及销毁(终止)进程。

2. 使用Process类编译和运行Java程序

首先,让我们看一个借助Process API编译并运行另一个Java程序的示例:

@Test
public void whenExecutedFromAnotherProgram_thenSourceProgramOutput3() throws IOException {
    Process process = Runtime.getRuntime().exec("javac -cp src src\\main\\java\\cn\\tuyucheng\\taketoday\\java9\\process\\OutputStreamExample.java");
    process = Runtime.getRuntime().exec("java -cp src/main/java cn.tuyucheng.taketoday.java9.process.OutputStreamExample");
    BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()));
    int value = Integer.parseInt(output.readLine());

    assertEquals(3, value);
}

因此,在现有Java代码中执行Java代码的应用实际上是无限的。

3. 创建进程

一般来说,我们的Java应用程序可以调用在我们的计算机系统内运行的任何应用程序,但受到操作系统的限制。

因此我们可以执行应用程序。那么,让我们看看利用Process API可以运行哪些不同的用例。

简而言之,ProcessBuilder类允许我们在应用程序中创建子进程。

让我们看一个打开基于Windows的记事本应用程序的演示:

ProcessBuilder builder = new ProcessBuilder("notepad.exe");
Process process = builder.start();

4. 销毁进程

Process类还为我们提供了销毁子进程或进程的方法,但是,应用程序如何被销毁与平台相关

接下来,让我们用实际的例子来举例说明不同的用例。

4.1 通过引用销毁进程

假设我们正在使用Windows操作系统,并且想要生成记事本应用程序并销毁它。

和以前一样,我们可以使用ProcessBuilder类和start()方法创建记事本应用程序的实例。然后,我们可以在Process对象上调用destroy()方法。

4.2 通过ID销毁进程

通常,我们可以终止操作系统中正在运行的进程,而这些进程可能不是由我们的应用程序创建的。执行此操作时要小心谨慎,因为在不知情的情况下破坏关键进程可能会破坏操作系统的稳定性

首先,我们需要通过检查任务管理器并找出pid来找出当前正在运行的进程的进程ID。

让我们看一个例子:

long pid = /* PID to kill */;
Optional<ProcessHandle> optionalProcessHandle = ProcessHandle.of(pid);
optionalProcessHandle.ifPresent(processHandle -> processHandle.destroy());

4.3 强制销毁进程

当我们执行destroy()方法时,它会终止子进程,就像我们在文章前面看到的那样。在destroy()不起作用的情况下,我们可以选择destroyForcibly()

值得注意的是,我们应该始终先使用destroy()方法。随后,我们可以使用isAlive()方法对子进程进行快速检查。

如果返回true则执行destroyForcibly():

ProcessBuilder builder = new ProcessBuilder("notepad.exe");
Process process = builder.start();
process.destroy();
if (process.isAlive()) {
    process.destroyForcibly();
}

5. 等待进程完成

我们还有两个重载方法,通过它们我们可以确保等待一个进程完成。

5.1 waitfor()

简而言之,当此方法执行时,它会将当前执行进程线程置于阻塞等待状态,直到子进程终止

那么,让我们看看它的实际效果:

ProcessBuilder builder = new ProcessBuilder("notepad.exe");
Process process = builder.start();
assertThat(process.waitFor() >= 0);

可以看到,当前线程等待子进程线程结束,一旦子进程结束,当前线程才会继续执行。

5.2 waitfor(long timeOut, TimeUnit time)

通常,执行此方法会使当前执行进程线程处于阻塞等待状态,直到子进程终止或超时。

那么,让我们在实践中看看这一点:

ProcessBuilder builder = new ProcessBuilder("notepad.exe");
Process process = builder.start();
assertFalse(process.waitFor(1, TimeUnit.SECONDS));

从上面的例子我们可以看出,当前线程要继续执行,它将继续等待子进程线程结束或指定的时间间隔已经过去。

当执行此方法时,如果子进程已退出,则返回布尔值true;如果在子进程退出之前等待时间已经过去,则返回布尔值false。

6. exitValue()

当运行此方法时,当前线程将不会等待子进程终止或销毁,但是,如果子进程未终止,它将抛出IllegalThreadStateException。

另一种方法是,如果子进程已成功终止,则将导致进程的退出值,它可以是任何可能的正整数。

那么,让我们看一个当子进程成功终止时exitValue()方法返回正整数的例子:

@Test
public void givenSubProcess_whenCurrentThreadWillNotWaitIndefinitelyforSubProcessToEnd_thenProcessExitValueReturnsGrt0() throws IOException {
    ProcessBuilder builder = new ProcessBuilder("notepad.exe");
    Process process = builder.start();
    assertThat(process.exitValue() >= 0);
}

7. isAlive()

通常,当我们想要执行业务处理时,无论进程是否处于活动状态都是主观的,我们可以执行快速检查以查找进程是否处于活动状态并返回布尔值。

让我们看一个简单的例子:

ProcessBuilder builder = new ProcessBuilder("notepad.exe");
Process process = builder.start();
Thread.sleep(10000);
process.destroy();
assertTrue(process.isAlive());

8. 处理进程流

默认情况下,创建的子进程没有自己的终端或控制台,其所有标准I/O(即stdin、stdout、stderr)操作都将发送到父进程。因此,父进程可以使用这些流向子进程提供输入并从子进程获取输出。

因此,这给了我们极大的灵活性,因为它使我们能够控制子进程的输入/输出。

8.1 getErrorStream()

有趣的是,我们可以获取子进程产生的错误并在此基础上执行业务处理。

之后,我们可以根据自己的需求执行具体的业务处理检查。

我们来看一个例子:

@Test
public void givenSubProcess_whenEncounterError_thenErrorStreamNotNull() throws IOException {
    Process process = Runtime.getRuntime().exec("javac -cp src src\\main\\java\\cn\\tuyucheng\\taketoday\\java9\\process\\ProcessCompilationError.java");
    BufferedReader error = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    String errorString = error.readLine();
    assertNotNull(errorString);
}

8.2 getInputStream()

我们还可以获取子进程生成的输出并在父进程中使用它,从而允许在进程之间共享信息:

@Test
public void givenSourceProgram_whenReadingInputStream_thenFirstLineEquals3() throws IOException {
    Process process = Runtime.getRuntime().exec("javac -cp src src\\main\\java\\cn\\tuyucheng\\taketoday\\java9\\process\\OutputStreamExample.java");
    process = Runtime.getRuntime().exec("java -cp  src/main/java cn.tuyucheng.taketoday.java9.process.OutputStreamExample");
    BufferedReader output = new BufferedReader(new InputStreamReader(process.getInputStream()));
    int value = Integer.parseInt(output.readLine());

    assertEquals(3, value);
}

8.3 getOutputStream()

我们可以从父进程向子进程发送输入:

Writer w = new OutputStreamWriter(process.getOutputStream(), "UTF-8");
w.write("send to childn");

8.4 过滤进程流

这是与选择性运行的进程进行交互的一个非常有效的用例。

Process类为我们提供了根据某个谓词有选择地过滤正在运行的进程的功能。

之后我们就可以针对这个选择性进程集进行业务操作了:

@Test
public void givenRunningProcesses_whenFilterOnProcessIdRange_thenGetSelectedProcessPid() {
    assertThat(((int) ProcessHandle.allProcesses()
        .filter(ph -> (ph.pid() > 10000 && ph.pid() < 50000))
        .count()) > 0);
}

9. Java 9改进

Java 9引入了获取有关当前进程和衍生进程信息的新选项和方法,让我们深入了解并详细探索每个功能。

9.1 当前Java进程信息

我们现在可以通过java.lang.ProcessHandle.Info API获取有关该进程的大量信息:

  • 用于启动进程的命令
  • 命令的参数
  • 进程启动的时间
  • 该应用和创建该应用的用户所花费的总时间

例如,我们可以这样做:

private static void infoOfCurrentProcess() {
    ProcessHandle processHandle = ProcessHandle.current();
    ProcessHandle.Info processInfo = processHandle.info();

    log.info("PID: " + processHandle.pid());
    log.info("Arguments: " + processInfo.arguments());
    log.info("Command: " + processInfo.command());
    log.info("Instant: " + processInfo.startInstant());
    log.info("Total CPU duration: " + processInfo.totalCpuDuration());
    log.info("User: " + processInfo.user());
}

需要注意的是,java.lang.ProcessHandle.Info是另一个接口java.lang.ProcessHandle中定义的公共接口,JDK提供程序(Oracle JDK、Open JDK、Zulu或其他)应以返回进程相关信息的方式实现这些接口。

输出取决于操作系统和Java版本,以下是输出示例:

16:31:24.784 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - PID: 22640
16:31:24.790 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - Arguments: Optional[[Ljava.lang.String;@2a17b7b6]
16:31:24.791 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - Command: Optional[/Library/Java/JavaVirtualMachines/jdk-13.0.1.jdk/Contents/Home/bin/java]
16:31:24.795 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - Instant: Optional[2021-08-31T14:31:23.870Z]
16:31:24.795 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - Total CPU duration: Optional[PT0.818115S]
16:31:24.796 [main] INFO  c.t.t.j.process.ProcessAPIEnhancements - User: Optional[username]

9.2 生成进程信息

还可以获取新生成的进程的进程信息,在这种情况下,生成进程并获取java.lang.Process实例后,我们对其调用toHandle()方法以获取java.lang.ProcessHandle实例。

其余细节与上面部分相同:

String javaCmd = ProcessUtils.getJavaCmd().getAbsolutePath();
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version");
Process process = processBuilder.inheritIO().start();
ProcessHandle processHandle = process.toHandle();

9.3 枚举系统中的活动进程

我们可以列出系统中当前进程可见的所有进程,返回的列表是所调用API的快照,因此某些进程可能在拍摄快照后终止,或者添加了新进程。

为了做到这一点,我们可以使用java.lang.ProcessHandle接口中提供的静态方法allProcesses(),它返回一个ProcessHandle流:

private static void infoOfLiveProcesses() {
    Stream<ProcessHandle> liveProcesses = ProcessHandle.allProcesses();
    liveProcesses.filter(ProcessHandle::isAlive)
        .forEach(ph -> {
            log.info("PID: " + ph.pid());
            log.info("Instance: " + ph.info().startInstant());
            log.info("User: " + ph.info().user());
        });
}

9.4 枚举子进程

有两种方法可以实现此目的:

  • 获取当前进程的直接子进程
  • 获取当前进程的所有后代

前者通过使用方法children()实现,后者通过使用方法descendants()实现:

private static void infoOfChildProcess() throws IOException {
    int childProcessCount = 5;
    for (int i = 0; i < childProcessCount; i++) {
        String javaCmd = ProcessUtils.getJavaCmd()
            .getAbsolutePath();
        ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version");
        processBuilder.inheritIO().start();
    }

    Stream<ProcessHandle> children = ProcessHandle.current()
        .children();
    children.filter(ProcessHandle::isAlive)
        .forEach(ph -> log.info("PID: {}, Cmd: {}", ph.pid(), ph.info()
            .command()));
    Stream<ProcessHandle> descendants = ProcessHandle.current()
        .descendants();
    descendants.filter(ProcessHandle::isAlive)
        .forEach(ph -> log.info("PID: {}, Cmd: {}", ph.pid(), ph.info()
            .command()));
}

9.5 进程终止时触发相关操作

我们可能想在进程终止时运行某些操作,这可以通过使用java.lang.ProcessHandle接口中的onExit()方法来实现。该方法返回一个CompletableFuture,它提供了在CompletableFuture完成时触发依赖操作的能力

这里,CompletableFuture表示该进程已完成,但进程是否成功完成并不重要,我们调用CompletableFuture上的get()方法来等待其完成:

private static void infoOfExitCallback() throws IOException, InterruptedException, ExecutionException {
    String javaCmd = ProcessUtils.getJavaCmd()
        .getAbsolutePath();
    ProcessBuilder processBuilder = new ProcessBuilder(javaCmd, "-version");
    Process process = processBuilder.inheritIO()
        .start();
    ProcessHandle processHandle = process.toHandle();

    log.info("PID: {} has started", processHandle.pid());
    CompletableFuture onProcessExit = processHandle.onExit();
    onProcessExit.get();
    log.info("Alive: " + processHandle.isAlive());
    onProcessExit.thenAccept(ph -> log.info("PID: {} has stopped", ph.pid()));
}

onExit()方法也可在java.lang.Process接口中使用。

10. 总结

在本文中,我们介绍了Java中Process API的大部分重要特性,在此过程中,我们还讨论了Java 9中引入的新改进。

Show Disqus Comments

Post Directory

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