Java9 進程API詳細介紹

官方在JEP 102中引進新的進程API來增強java.lang.Process 類,並且引進java.lang.ProcessHandle 及其嵌套接口Info 來讓開發者逃離時常因爲要獲取一個本地進程的PID而不得不使用本地代碼的窘境。本文將詳細介紹這些新特性。

一張逼格滿滿的圖

1、ProcessHandle 與 ProcessHandle.Info

Java 9 爲抽象Process 類增加了許多新方法,通過這些方法可以識別直接子進程與所有後代進程, 獲取進程的PID、 獲取進程的快照、獲取CompletableFuture 實例來接收進程結束時的異步通知,以及更多特性的獲取:

Stream<ProcessHandle> children()
Stream<ProcessHandle> descendants()
long getPid()
ProcessHandle.Info info()
CompletableFuture<Process> onExit()
boolean supportsNormalTermination()
ProcessHandle toHandle()

可以看出,超過半數的方法是需要結合ProcessHandle 接口來使用的, ProcessHandle 接口可以識別並控制本地進程。例如,toHandle() 方法可以返回ProcessHandle 的具體實現對象和與之關聯的進程,ProcessHandle 中聲明的方法如下:

static Stream<ProcessHandle> allProcesses()
Stream<ProcessHandle> children()
int compareTo(ProcessHandle other)
static ProcessHandle current()
Stream<ProcessHandle> descendants()
boolean destroy()
boolean destroyForcibly()
long getPid()
ProcessHandle.Info info()
boolean isAlive()
static Optional<ProcessHandle> of(long pid)
CompletableFuture<ProcessHandle> onExit()
Optional<ProcessHandle> parent()
boolean supportsNormalTermination()

Process 中的方法以方法名通過調用toHandle() 方法委派給ProcessHandle 接口。例如,調用getPid() 是調用 toHandle().getPid(),調用info()是調用 toHandle().info(),返回的是ProcessHandle.Info 對象,其嵌套接口Info 提供以下方法列表:

Optional<String[]> arguments()
Optional<String> command()
Optional<String> commandLine()
Optional<Instant> startInstant()
Optional<Duration> totalCpuDuration()
Optional<String> user()

每個方法返回java.util.Optional 實例,可能是null或非空對象引用, 大家都知道這樣能有效避免java.lang.NullPointerException。您將在下文詳細瞭解這些方法。

2、獲取PID

Processlong getPid() 方法返回特定進程的PID。 之所以返回值是long類型而不是int類型,是因爲PID是無符號整型。 最大的正值整型約爲200萬,但是Linux系統可以容納大概400萬個PID。

下面是獲取PID的方式:

import java.io.IOException;

public class ProcessDemo
{
   public static void main(String[] args) throws IOException
   {
      Process p = new ProcessBuilder("notepad.exe").start();
      System.out.println(p.getPid());
   }
}

java.lang.ProcessBuilder 類(引進於Java 5) 爲Windows的notepad.exe程序構建了一個進程。用start() 方法開啓,返回了一個Process 對象來與新進程進行交互。 然後再調用getPid() 方法來獲取PID。

產生新進程的方式

在Java 5之前,產生新進程的唯一方式是使用Runtime.getRuntime().exec() ,而現在更好的方式是使用ProcessBuilder

編譯ProcessDemo.java:

javac ProcessDemo.java

運行ProcessDemo.java:

java ProcessDemo

你可以在進程管理器看到一個新的進程notepad.exe 正在運行,並且可以看到它的PID。

9480 或者其他無符號整型

從進程句柄獲取PID

如果有一個ProcessHandle 對象,可以通過調用getPid() 來獲取PID。

你一定很想知道在調用這個方法的時候如果進程無法啓動或者已經意外終止會發生什麼,第一種情況, start() 拋出 java.io.IOException ,第二種情況, getPid() 繼續在進程終止後返回PID。

3、獲取進程信息

ProcessHandle.Info 定義了一些返回進程信息的方法,比如,進程的可執行路徑名,進程的開啓時間,開啓進程的用戶等等。

以下代碼開啓了一個進程,並將此進程的一些信息輸出:

import java.io.IOException;

import java.time.Duration;
import java.time.Instant;

public class ProcessDemo
{
   public static void main(String[] args) 
      throws InterruptedException, IOException
   {
      dumpProcessInfo(ProcessHandle.current());
      Process p = new ProcessBuilder("notepad.exe", "C:\\temp\\names.txt").start();
      dumpProcessInfo(p.toHandle());
      p.waitFor();
      dumpProcessInfo(p.toHandle());
   }

   static void dumpProcessInfo(ProcessHandle ph)
   {
      System.out.println("PROCESS INFORMATION");
      System.out.println("===================");
      System.out.printf("Process id: %d%n", ph.getPid());
      ProcessHandle.Info info = ph.info();
      System.out.printf("Command: %s%n", info.command().orElse(""));
      String[] args = info.arguments().orElse(new String[]{});
      System.out.println("Arguments:");
      for (String arg: args)
         System.out.printf("   %s%n", arg);
      System.out.printf("Command line: %s%n", info.commandLine().orElse(""));
      System.out.printf("Start time: %s%n", 
                        info.startInstant().orElse(Instant.now()).toString());
      System.out.printf("Run time duration: %sms%n",
                        info.totalCpuDuration()
                            .orElse(Duration.ofMillis(0)).toMillis());
      System.out.printf("Owner: %s%n", info.user().orElse(""));
      System.out.println();
   }
}

main() 方法裏面首先調用 ProcessHandle.current() 來獲取當前進程的句柄,然後用dumpProcessInfo() 方法來輸出dump進程的詳細信息。接下來啓動notepad.exe ,並且dump其進程信息。等到notepad.exe 終止之後,再一次dump了它的信息。

dumpProcessInfo() 方法首先輸出頭標識信息,隨後輸出PID,隨後獲取ProcessHandle.Info 引用。接下來,調用command()和其他Info方法,輸出它們的值。 如果方法返回null(因爲信息不可用),則通過OptionalorElse()方法來返回信息。

以下是輸出內容,期間可以看到notepad窗口:

PROCESS INFORMATION
===================
Process id: 1140
Command: C:\PROGRA~1\Java\jdk-9\bin\java.exe
Arguments:
Command line: 
Start time: 2017-03-02T22:24:40.998Z
Run time duration: 890ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 5516
Command: C:\Windows\System32\notepad.exe
Arguments:
Command line: 
Start time: 2017-03-02T22:24:41.763Z
Run time duration: 0ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 5516
Command: 
Arguments:
Command line: 
Start time: 2017-03-02T22:24:41.763Z
Run time duration: 234ms
Owner: jeff\jeffrey

第三部分的PROCESS INFORMATION遲遲沒有出現,直到notepad界面消失纔出現。Info的arguments() 方法沒有向C:\temp\names.txt 文件裏打印命令行,可能是因爲此時信息是不可用的,抑或是因爲出現了bug。在進程結束之後,command()方法返回null。最後,當command()方法或者arguments() 方法其中之一返回了null,commandLine()方法也將返回null。

4、獲取所有進程的信息

ProcessHandle 中的allProcesses() 方法以Java8中Stream API的方式返回當前系統中所有可見的進程句柄。下面的代碼展示瞭如何使用Stream來獲取進程句柄, 取前四個進程,dump出它們的信息。

import java.io.IOException;

import java.time.Duration;
import java.time.Instant;

public class ProcessDemo
{
   public static void main(String[] args)
   {
      ProcessHandle.allProcesses()
                   .filter(ph -> ph.info().command().isPresent())
                   .limit(4)
                   .forEach((process) -> dumpProcessInfo(process));
   }

   static void dumpProcessInfo(ProcessHandle ph)
   {
      System.out.println("PROCESS INFORMATION");
      System.out.println("===================");
      System.out.printf("Process id: %d%n", ph.getPid());
      ProcessHandle.Info info = ph.info();
      System.out.printf("Command: %s%n", info.command().orElse(""));
      String[] args = info.arguments().orElse(new String[]{});
      System.out.println("Arguments:");
      for (String arg: args)
         System.out.printf("   %s%n", arg);
      System.out.printf("Command line: %s%n", info.commandLine().orElse(""));
      System.out.printf("Start time: %s%n", 
                        info.startInstant().orElse(Instant.now()).toString());
      System.out.printf("Run time duration: %sms%n",
                        info.totalCpuDuration()
                            .orElse(Duration.ofMillis(0)).toMillis());
      System.out.printf("Owner: %s%n", info.user().orElse(""));
      System.out.println();
   }
}

main()方法中調用allProcesses()方法,從路徑名顯示地將線程句柄流鏈式導入到一個filter之後再包裝到一個新的線程句柄流。 (在我的環境中,當進程終止之後就不會打印出路徑) limit(4) 方法可以截取不超過4個進程來放入流中。最後,迭代出它們的所有信息。

以下是輸出結果:

PROCESS INFORMATION
===================
Process id: 8036
Command: C:\Windows\explorer.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:14.436Z
Run time duration: 299328ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 10200
Command: C:\Windows\System32\dllhost.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:16.255Z
Run time duration: 2000ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 1544
Command: C:\Program Files (x86)\WNSS\WNSS.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:21.708Z
Run time duration: 862375ms
Owner: jeff\jeffrey

PROCESS INFORMATION
===================
Process id: 8156
Command: C:\Users\jeffrey\AppData\Local\SweetLabs App Platform\Engine\ServiceHostAppUpdater.exe
Arguments:
Command line: 
Start time: 2017-03-02T16:21:24.302Z
Run time duration: 2468ms
Owner: jeff\jeffrey

ProcessHandle的 children() 方法和 descendents() 方法運行結果很像allProcesses() f方法,除了它們是否是靜態方法,以及返回值類型不一樣之外,它們之間有一個集合從屬關係,children() 是descendents()的子集,descendents()是allProcesses()的子集。

5、進程終止的觸發機制

最後, ProcessHandle的 onExit() 方法返回java.util.concurrent.CompletableFuture讓進程在終止時進行同步或異步操作成爲可能。

在一個進程終止的時候打印出它的PID:

import java.io.IOException;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class ProcessDemo
{
   public static void main(String[] args) 
      throws ExecutionException, InterruptedException, IOException
   {
      Process p = new ProcessBuilder("notepad.exe").start();
      ProcessHandle ph = p.toHandle();
      CompletableFuture<ProcessHandle> onExit = ph.onExit();
      onExit.get();
      onExit.thenAccept(ph_ -> System.out.printf("PID %d terminated%n", ph_.getPid()));
   }
}

main() 方法首先創建了一個notepad.exe進程。隨後,又獲取了這個進程的句柄,用這個句柄又得到了CompletableFuture。onExit.get() 在 main()獲取到進程終止信號後會進行一些操作。

PID 7460 terminated

6、結論

Java 9的 Process API 增強早就該和Java一起出現並受到歡迎。雖然這盤文章介紹了一些它的API,但是更多的還是需要你去探索,比如,supportsNormalTermination() 方法或者parent() 方法的用法等等。

原文:https://www.javaworld.com/article/3176874/java-language/java-9s-other-new-enhancements-part-3.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章