官方在JEP 102中引进新的进程API来增强java.lang.Process
类,并且引进java.lang.ProcessHandle
及其嵌套接口Info
来让开发者逃离时常因为要获取一个本地进程的PID而不得不使用本地代码的窘境。本文将详细介绍这些新特性。
1、ProcessHandle 与 ProcessHandle.Info
Java 9 为抽象Process
类增加了许多新方法,通过这些方法可以识别直接子进程与所有后代进程, 获取进程的PID、 获取进程的快照、获取CompletableFuture
实例来接收进程结束时的异步通知,以及更多特性的获取:
Stream children()
Stream descendants()
long getPid()
ProcessHandle.Info info()
CompletableFuture onExit()
boolean supportsNormalTermination()
ProcessHandle toHandle()
可以看出,超过半数的方法是需要结合ProcessHandle
接口来使用的, ProcessHandle
接口可以识别并控制本地进程。例如,toHandle()
方法可以返回ProcessHandle
的具体实现对象和与之关联的进程,ProcessHandle
中声明的方法如下:
static Stream allProcesses()
Stream children()
int compareTo(ProcessHandle other)
static ProcessHandle current()
Stream descendants()
boolean destroy()
boolean destroyForcibly()
long getPid()
ProcessHandle.Info info()
boolean isAlive()
static Optional of(long pid)
CompletableFuture onExit()
Optional 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 startInstant()
Optional totalCpuDuration()
Optional<String> user()
每个方法返回java.util.Optional
实例,可能是null或非空对象引用, 大家都知道这样能有效避免java.lang.NullPointerException
。您将在下文详细了解这些方法。
2、获取PID
Process
的long 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(因为信息不可用),则通过Optional
的orElse()
方法来返回信息。
以下是输出内容,期间可以看到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 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