Java 9 揭祕(12. Process API 更新)
Tips
做一個終身學習的人。
在本章中,主要介紹以下內容:
- Process API是什麼
- 如何建立本地程序
- 如何獲取新程序的資訊
- 如何獲取當前程序的資訊
- 如何獲取所有系統程序的資訊
- 如何設定建立,查詢和管理本地程序的許可權
一. Process API是什麼
Process API 由介面和類組成,用來與本地程序一起工作,使用API,可以做以下事情:
- 從Java程式碼中建立新的本地程序
- 獲取本地程序的程序控制代碼,無論它們是由Java程式碼還是通過其他方式建立
- 銷燬執行程序
- 查詢活動的程序及其屬性
- 獲取程序的子程序和父程序的列表
- 獲取本地程序的程序ID(PID)
- 獲取新建立的程序的輸入,輸出和錯誤流
- 等待程序終止
- 當程序終止時執行任務
Process API由java.lang包中的以下類和介面組成:
Runtime
ProcessBuilder
ProcessBuilder.Redirect
Process
ProcessHandle
ProcessHandle.Info
自Java 1.0以來,支援使用本地程序。Process
類的例項表示由Java程式建立的本地程序。 通過呼叫Runtime
類的exec()
方法啟動一個程序。
JDK 5.0添加了ProcessBuilder
類,JDK 7.0添加了ProcessBuilder.Redirect
的巢狀類。 ProcessBuilder
類的例項儲存一個程序的一組屬性。 呼叫其start()
Process
類的例項。 可以多次呼叫其start()
方法; 每次使用ProcessBuilder
例項中儲存的屬性啟動一個新程序。 在Java 5.0中,ProcessBuilder
類接管Runtime.exec()
方法來啟動新程序。
在Java 7和Java 8中的Process API中有一些改進,就是在Process
和ProcessBuilder
類中新增幾個方法。
在Java 9之前,Process API仍然缺乏對使用本地程序的基本支援,例如獲取程序的PID和所有者,程序的開始時間,程序使用了多少CPU時間,多少本地程序正在執行等。請注意,在Java 9之前,可以啟動本地程序並使用其輸入,輸出和錯誤流。 但是,無法使用未啟動的本地程序,無法查詢程序的詳細資訊。 為了更緊密地處理本地程序,Java開發人員不得不使用Java Native Interface(JNI)來編寫原生代碼。 Java 9使這些非常需要的功能與本地程序配合使用。
Java 9向Process API添加了一個名為ProcessHandle
的介面。 ProcessHandle
介面的例項標識一個本地程序; 它允許查詢程序狀態並管理程序。
比較Process
類和ProcessHandle
介面。 Process
類的一個例項表示由當前Java程式啟動的本地程序,而ProcessHandle
介面的例項表示本地程序,無論是由當前Java程式啟動還是以其他方式啟動。 在Java 9中,已經在Process
類中添加了幾種方法,這些方法也可以在新的ProcessHandle
介面中使用。 Process
類包含一個返回ProcessHandle
的toHandle()
方法。
ProcessHandle.Info
介面的例項表示程序屬性的快照。 請注意,程序由不同的作業系統不同地實現,因此它們的屬性不同。 過程的狀態可以隨時更改,例如,當程序獲得更多CPU時間時,程序使用的CPU時間增加。 要獲取程序的最新資訊,需要在需要時使用ProcessHandle
介面的info()
方法,這將返回一個新的ProcessHandle.Info
例項。
本章中的所有示例都在Windows 10中執行。當使用Windows 10或其他作業系統在機器上執行這些程式時,可能會得到不同的輸出。
二. 當前程序
ProcessHandle
介面的current()
靜態方法返回當前程序的控制代碼。 請注意,此方法返回的當前程序始終是正在執行程式碼的Java程序。
// Get the handle of the current process
ProcessHandle current = ProcessHandle.current();
獲取當前程序的控制代碼後,可以使用ProcessHandle
介面的方法獲取有關程序的詳細資訊。
Tips
你不能殺死當前程序。 嘗試通過使用ProcessHandle
介面的destroy()
或destroyForcibly()
方法來殺死當前程序會導致IllegalStateException
異常。
三. 查詢程序狀態
可以使用ProcessHandle
介面中的方法來查詢程序的狀態。 下表列出了該介面常用的簡單說明方法。 請注意,許多這些方法返回執行快照時程序狀態的快照。 不過,由於程序是以非同步方式建立,執行和銷燬的,所以當稍後使用其屬性時,所以無法保證程序仍然處於相同的狀態。
方法 | 描述 |
---|---|
static Stream<ProcessHandle> allProcesses() |
返回作業系統中當前程序可見的所有程序的快照。 |
Stream<ProcessHandle> children() |
返回程序當前直接子程序的快照。 使用descendants() 方法獲取所有級別的子級列表,例如子程序,孫子程序程序等。返回當前程序可見的作業系統中的所有程序的快照。 |
static ProcessHandle current() |
返回當前程序的ProcessHandle ,這是執行此方法呼叫的Java程序。 |
Stream<ProcessHandle> descendants() |
返回程序後代的快照。 與children() 方法進行比較,該方法僅返回程序的直接後代。 |
boolean destroy() |
請求程序被殺死。 如果成功請求終止程序,則返回true,否則返回false。 是否可以殺死程序取決於作業系統訪問控制。 |
boolean destroyForcibly() |
要求程序被強行殺死。 如果成功請求終止程序,則返回true,否則返回false。 殺死程序會立即強制終止程序,而正常終止則允許程序徹底關閉。 是否可以殺死程序取決於作業系統訪問控制。 |
long getPid() |
返回由作業系統分配的程序的本地程序ID(PID)。 注意,PID可以由作業系統重複使用,因此具有相同PID的兩個處理控制代碼可能不一定代表相同的過程。 |
ProcessHandle.Info info() |
返回有關程序資訊的快照。 |
boolean isAlive() |
如果此ProcessHandle 表示的程序尚未終止,則返回true,否則返回false。 請注意,在成功請求終止程序後,此方法可能會返回一段時間,因為程序將以非同步方式終止。 |
static Optional<ProcessHandle> of(long pid) |
返回現有本地程序的Optional<ProcessHandle> 。 如果具有指定pid的程序不存在,則返回空的Optional 。 |
CompletableFuture <ProcessHandle> onExit() |
返回一個用於終止程序的CompletableFuture<ProcessHandle> 。 可以使用返回的物件來新增在程序終止時執行的任務。 在當前程序中呼叫此方法會引發IllegalStateException 異常。 |
Optional<ProcessHandle> parent() |
返回父程序的Optional<ProcessHandle> 。 |
boolean supportsNormalTermination() |
如果destroy() 的實現正常終止程序,則返回true。 |
下表列出ProcessHandle.Info
巢狀介面的方法和描述。 此介面的例項包含有關程序的快照資訊。 可以使用ProcessHandle
介面或Process
類的info()
方法獲取ProcessHandle.Info
。 介面中的所有方法都返回一個Optional
。
方法 | 描述 |
---|---|
Optional<String[]> arguments() |
返回程序的引數。 該過程可能會更改啟動後傳遞給它的原始引數。 在這種情況下,此方法返回更改的引數。 |
Optional<String> command() |
返回程序的可執行路徑名。 |
Optional<String> commandLine() |
它是一個程序的組合命令和引數的便捷的方法。如果command() 和arguments() 方法都沒有返回空Optional , 它通過組合從command() 和arguments() 方法返回的值來返回程序的命令列。 |
Optional<Instant> startInstant() |
返回程序的開始時間。 如果作業系統沒有返回開始時間,則返回一個空Optional 。 |
Optional<Duration> totalCpuDuration() |
返回程序使用的CPU時間。 請注意,程序可能執行很長時間,但可能使用很少的CPU時間。 |
Optional<String> user() |
返回程序的使用者。 |
現在是時候看到ProcessHandle
和ProcessHandle.Info
介面的實際用法。 本章中的所有類都在com.jdojo.process.api模組中,其宣告如下所示。
// module-info.java
module com.jdojo.process.api {
exports com.jdojo.process.api;
}
接下來包含CurrentProcessInfo
類的程式碼。 它的printInfo()
方法將ProcessHandle
作為引數,並列印程序的詳細資訊。 我們還在其他示例中使用此方法列印程序的詳細資訊。main()
方法獲取執行程序的當前程序的控制代碼,這是一個Java程序,並列印其詳細資訊。 你可能會得到不同的輸出。 以下是當程式在Windows 10上執行時生成輸出。
// CurrentProcessInfo.java
package com.jdojo.process.api;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
public class CurrentProcessInfo {
public static void main(String[] args) {
// Get the handle of the current process
ProcessHandle current = ProcessHandle.current();
// Print the process details
printInfo(current);
}
public static void printInfo(ProcessHandle handle) {
// Get the process ID
long pid = handle.getPid();
// Is the process still running
boolean isAlive = handle.isAlive();
// Get other process info
ProcessHandle.Info info = handle.info();
String command = info.command().orElse("");
String[] args = info.arguments()
.orElse(new String[]{});
String commandLine = info.commandLine().orElse("");
ZonedDateTime startTime = info.startInstant()
.orElse(Instant.now())
.atZone(ZoneId.systemDefault());
Duration duration = info.totalCpuDuration()
.orElse(Duration.ZERO);
String owner = info.user().orElse("Unknown");
long childrenCount = handle.children().count();
// Print the process details
System.out.printf("PID: %d%n", pid);
System.out.printf("IsAlive: %b%n", isAlive);
System.out.printf("Command: %s%n", command);
System.out.printf("Arguments: %s%n", Arrays.toString(args));
System.out.printf("CommandLine: %s%n", commandLine);
System.out.printf("Start Time: %s%n", startTime);
System.out.printf("CPU Time: %s%n", duration);
System.out.printf("Owner: %s%n", owner);
System.out.printf("Children Count: %d%n", childrenCount);
}
}
列印輸出為:
PID: 8692
IsAlive: true
Command: C:\java9\bin\java.exe
Arguments: []
CommandLine:
Start Time: 2016-11-27T12:28:20.611-06:00[America/Chicago]
CPU Time: PT0.296875S
Owner: kishori\ksharan
Children Count: 1
四. 比較程序
比較兩個程序是否相等等或順序是否相同是棘手的。 不能依賴PID來處理相同的程序。 作業系統在程序終止後重用PID。 可以與PID一起檢查流程的開始時間;如果兩者相同,則兩個過程可能相同。 ProcessHandle
介面的預設實現的equals()
方法檢查以下三個資訊,以使兩個程序相等:
- 對於這兩個程序,
ProcessHandle
介面的實現類必須相同。 - 程序必須具有相同的PID。
- 程序必須同一時間啟動。
Tips
在ProcessHandle
介面中使用compareTo()
方法的預設實現對於排序來說並不是很有用。 它比較了兩個程序的PID。
五. 建立程序
需要使用ProcessBuilder
類的例項來啟動一個新程序。 該類包含幾個方法來設定程序的屬性。 呼叫start()
方法啟動一個新程序。 start()
方法返回一個Process
物件,可以使用它來處理程序的輸入,輸出和錯誤流。 以下程式碼段建立一個ProcessBuilder
在Windows上啟動JVM:
ProcessBuilder pb = new ProcessBuilder()
.command("C:\\java9\\bin\\java.exe",
"--module-path",
"myModulePath",
"--module",
"myModule/className")
.inheritIO();
有兩種方法來設定這個新程序的命令和引數:
- 可以將它們傳遞給
ProcessBuilder
類的建構函式。 - 可以使用
command()
方法。
沒有引數的command()
方法返回在ProcessBuilder
中命令的設定的。 帶有引數的其他版本 —— 一個帶有一個String
的可變引數,一個帶有List<String>
的版本,都用於設定命令和引數。 該方法的第一個引數是命令路徑,其餘的是命令的引數。
新程序有自己的輸入,輸出和錯誤流。 inheritIO()
方法將新程序的輸入,輸出和錯誤流設定為與當前程序相同。 ProcessBuilder
類中有幾個redirectXxx()
方法可以為新程序定製標準I/O,例如將標準錯誤流設定為檔案,因此所有錯誤都會記錄到檔案中。 配置程序的所有屬性後,可以呼叫start()
來啟動程序:
// Start a new process
Process newProcess = pb.start();
可以多次呼叫ProcessBuilder
類的start()
方法來啟動與之前保持的相同屬性的多個程序。 這具有效能優勢,可以建立一個ProcessBuilder
例項,並重復使用它來多次啟動相同的程序。
可以使用Process
類的toHandle()
方法獲取程序的程序控制代碼:
// Get the process handle
ProcessHandle handle = newProcess.toHandle();
可以使用程序控制代碼來銷燬程序,等待程序完成,或查詢程序的狀態和屬性,如其子程序,後代,父程序,使用的CPU時間等。有關程序的資訊,對程序的控制取決於作業系統訪問控制。
建立可以在所有作業系統上執行的程序都很棘手。 可以建立一個新程序啟動新的JVM來執行一個類。
如下包含一個Job
類的程式碼。 它的main()
方法需要兩個引數:睡眠間隔和睡眠持續時間(以秒為單位)。 如果沒有引數傳遞,該方法將使用5秒和60秒作為預設值。 在第一部分中,該方法嘗試提取第一個和第二個引數(如果指定)。 在第二部分中,它使用ProcessHandle.current()
方法獲取當前程序執行此方法的程序控制代碼。 它讀取當前程序的PID並列印包括PID,睡眠間隔和睡眠持續時間的訊息。 最後,它開始一個for迴圈,並持續休眠睡眠間隔,直到達到睡眠持續時間。 在迴圈的每次迭代中,它列印一條訊息。
// Job.java
package com.jdojo.process.api;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* An instance of this class is used as a job that sleeps at a
* regular interval up to a maximum duration. The sleep
* interval in seconds can be specified as the first argument
* and the sleep duration as the second argument while running.
* this class. The default sleep interval and sleep duration
* are 5 seconds and 60 seconds, respectively. If these values
* are less than zero, zero is used instead.
*/
public class Job {
// The job sleep interval
public static final long DEFAULT_SLEEP_INTERVAL = 5;
// The job sleep duration
public static final long DEFAULT_SLEEP_DURATION = 60;
public static void main(String[] args) {
long sleepInterval = DEFAULT_SLEEP_INTERVAL;
long sleepDuration = DEFAULT_SLEEP_DURATION;
// Get the passed in sleep interval
if (args.length >= 1) {
sleepInterval = parseArg(args[0], DEFAULT_SLEEP_INTERVAL);
if (sleepInterval < 0) {
sleepInterval = 0;
}
}
// Get the passed in the sleep duration
if (args.length >= 2) {
sleepDuration = parseArg(args[1], DEFAULT_SLEEP_DURATION);
if (sleepDuration < 0) {
sleepDuration = 0;
}
}
long pid = ProcessHandle.current().getPid();
System.out.printf("Job (pid=%d) info: Sleep Interval" +
"=%d seconds, Sleep Duration=%d " +
"seconds.%n",
pid, sleepInterval, sleepDuration);
for (long sleptFor = 0; sleptFor < sleepDuration;
sleptFor += sleepInterval) {
try {
System.out.printf("Job (pid=%d) is going to" +
" sleep for %d seconds.%n",
pid, sleepInterval);
// Sleep for the sleep interval
TimeUnit.SECONDS.sleep(sleepInterval);
} catch (InterruptedException ex) {
System.out.printf("Job (pid=%d) was " +
"interrupted.%n", pid);
}
}
}
/**
* Starts a new JVM to run the Job class.
* @param sleepInterval The sleep interval when the Job
* class is run. It is passed to the JVM as the first
* argument.
* @param sleepDuration The sleep duration for the Job
* class. It is passed to the JVM as the second argument.
* @return The new process reference of the newly launched
* JVM or null if the JVM cannot be launched.
*/
public static Process startProcess(long sleepInterval,
long sleepDuration) {
// Store the command to launch a new JVM in a
// List<String>
List<String> cmd = new ArrayList<>();
// Add command components in order
addJvmPath(cmd);
addModulePath(cmd);
addClassPath(cmd);
addMainClass(cmd);
// Add arguments to run the class
cmd.add(String.valueOf(sleepInterval));
cmd.add(String.valueOf(sleepDuration));
// Build the process attributes
ProcessBuilder pb = new ProcessBuilder()
.command(cmd)
.inheritIO();
String commandLine = pb.command()
.stream()
.collect(Collectors.joining(" "));
System.out.println("Command used:\n" + commandLine);
// Start the process
Process p = null;
try {
p = pb.start();
} catch (IOException e) {
e.printStackTrace();
}
return p;
}
/**
* Used to parse the arguments passed to the JVM, which
* in turn is passed to the main() method.
* @param valueStr The string value of the argument
* @param defaultValue The default value of the argument if
* the valueStr is not an integer.
* @return valueStr as a long or the defaultValue if
* valueStr is not an integer.
*/
private static long parseArg(String valueStr,
long defaultValue) {
long value = defaultValue;
if (valueStr != null) {
try {
value = Long.parseLong(valueStr);
} catch (NumberFormatException e) {
// no action needed
}
}
return value;
}
/**
* Adds the JVM path to the command list. It first attempts
* to use the command attribute of the current process;
* failing that it relies on the java.home system property.
* @param cmd The command list
*/
private static void addJvmPath(List<String> cmd) {
// First try getting the command to run the current JVM
String jvmPath = ProcessHandle.current()
.info()
.command().orElse("");
if(jvmPath.length() > 0) {
cmd.add(jvmPath);
} else {
// Try composing the JVM path using the java.home
// system property
final String FILE_SEPARATOR =
System.getProperty("file.separator");
jvmPath = System.getProperty("java.home") +
FILE_SEPARATOR + "bin" +
FILE_SEPARATOR + "java";
cmd.add(jvmPath);
}
}
/**
* Adds a module path to the command list.
* @param cmd The command list
*/
private static void addModulePath(List<String> cmd) {
String modulePath =
System.getProperty("jdk.module.path");
if(modulePath != null && modulePath.trim().length() > 0) {
cmd.add("--module-path");
cmd.add(modulePath);
}
}
/**
* Adds class path to the command list.
* @param cmd The command list
*/
private static void addClassPath(List<String> cmd) {
String classPath = System.getProperty("java.class.path");
if(classPath != null && classPath.trim().length() > 0) {
cmd.add("--class-path");
cmd.add(classPath);
}
}
/**
* Adds a main class to the command list. Adds
* module/className or just className depending on whether
* the Job class was loaded in a named module or unnamed
* module
* @param cmd The command list
*/
private static void addMainClass(List<String> cmd) {
Class<Job> cls = Job.class;
String className = cls.getName();
Module module = cls.getModule();
if(module.isNamed()) {
String moduleName = module.getName();
cmd.add("--module");
cmd.add(moduleName + "/" + className);
} else {
cmd.add(className);
}
}
}
Job
類包含一個啟動新程序的startProcess(long sleepInterval,long sleepDuration)
方法。 它以Job
類作為主類啟動一個JVM。 將睡眠間隔和持續時間作為引數傳遞給JVM。 該方法嘗試構建一個從JDK_HOME\bin目錄下啟動java的命令。 如果Job
類被載入到一個命名的模組中,它將生成一個如下命令:
JDK_HOME\bin\java --module-path <module-path> --module com.jdojo.process.api/com.jdojo.process.api.Job <sleepInterval> <sleepDuration>
如果Job
類被載入到一個未命名的模組中,它將嘗試構建如下命令:
JDK_HOME\bin\java -class-path <class-path> com.jdojo.process.api.Job <sleepInterval> <sleepDuration>
startProcess()
方法列印用於啟動程序的命令,嘗試啟動程序,並返回程序引用。
addJvmPath()
方法將JVM路徑新增到命令列表中。 它嘗試獲取當前JVM程序的命令作為新程序的JVM路徑。 如果它不可用,將嘗試從java.home
系統屬性構建它。
Job
類包含幾個實用程式方法,用於構成命令的一部分並解析引數並傳遞給main()
方法。 具體請參考Javadoc的說明。
如果要啟動一個新程序,執行15秒鐘並且每5秒鐘喚醒,可以使用Job
類的startProcess()
方法:
// Start a process that runs for 15 seconds
Process p = Job.startProcess(5, 15);
可以使用CurrentProcessInfo
類的printInfo()
方法來列印程序細節:
// Get the handle of the current process
ProcessHandle handle = p.toHandle();
// Print the process details
CurrentProcessInfo.printInfo(handle);
當程序終止時,可以使用ProcessHandle的onExit()
方法的返回值來執行任務。
CompletableFuture<ProcessHandle> future = handle.onExit();
// Print a message when process terminates
future.thenAccept((ProcessHandle ph) -> {
System.out.printf("Job (pid=%d) terminated.%n", ph.getPid());
});
可以等待新程序終止:
// Wait for the process to terminate
future.get();
在這個例子中,future.get()
返回程序的ProcessHandle
。 沒有使用返回值,因為已經在handle
變數中。
下面包含了StartProcessTest
類的程式碼,它顯示瞭如何使用Job
類建立一個新程序。 在main()
方法中,它建立一個新程序,列印程序詳細資訊,向程序新增關閉任務,等待程序終止,並再次列印程序細節。 請注意,該程序執行15秒,但它僅使用0.359375秒的CPU時間,因為大多數時間程序的主執行緒正在休眠。 以下輸入結果當程式在Windows 10上執行時生成輸出。
// StartProcessTest.java
package com.jdojo.process.api;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class StartProcessTest {
public static void main(String[] args) {
// Start a process that runs for 15 seconds
Process p = Job.startProcess(5, 15);
if (p == null) {
System.out.println("Could not create a new process.");
return;
}
// Get the handle of the current process
ProcessHandle handle = p.toHandle();
// Print the process details
CurrentProcessInfo.printInfo(handle);
CompletableFuture<ProcessHandle> future = handle.onExit();
// Print a message when process terminates
future.thenAccept((ProcessHandle ph) -> {
System.out.printf("Job (pid=%d) terminated.%n", ph.getPid());
});
try {
// Wait for the process to complete
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// Print process details again
CurrentProcessInfo.printInfo(handle);
}
}
輸出結果為:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --class-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 5 15
PID: 10928
IsAlive: true
Command: C:\java9\bin\java.exe
Arguments: []
CommandLine:
Start Time: 2016-11-28T13:43:28.318-06:00[America/Chicago]
CPU Time: PT0S
Owner: kishori\ksharan
Children Count: 1
Job (pid=10928) info: Sleep Interval=5 seconds, Sleep Duration=15 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) is going to sleep for 5 seconds.
Job (pid=10928) terminated.
PID: 10928
IsAlive: false
Command:
Arguments: []
CommandLine:
Start Time: 2016-11-28T13:43:28.318-06:00[America/Chicago]
CPU Time: PT0.359375S
Owner: kishori\ksharan
Children Count: 0
六. 獲取程序控制代碼
有幾種方法來獲取本地程序的控制代碼。 對於由Java程式碼建立的程序,可以使用Process
類的toHandle()
方法獲取一個ProcessHandle
。 本地程序也可以從JVM外部建立。 ProcessHandle
介面包含以下方法來獲取本地程序的控制代碼:
static Optional<ProcessHandle> of(long pid)
static ProcessHandle current()
Optional<ProcessHandle> parent()
Stream<ProcessHandle> children()
Stream<ProcessHandle> descendants()
static Stream<ProcessHandle> allProcesses()
of()
靜態方法返回指定pid的Optional<ProcessHandle>
。 如果沒有此pid的程序,則返回一個空Optional
。 要使用此方法,需要知道程序的PID:
// Get the process handle of the process with the pid of 1234
Optional<ProcessHandle> handle = ProcessHandle.of(1234L);
靜態current()
方法返回當前程序的控制代碼,它始終是執行程式碼的Java程序。
parent()
方法返回父程序的控制代碼。 如果程序沒有父程序或父程序無法檢索,則返回一個空Optional
。
children()
方法返回程序的所有直接子程序的快照。 不能保證此方法返回的程序仍然存在。 請注意,一個不存在的程序沒有子程序。
descendants()
方法返回直接或間接程序的所有子程序的快照。
allProcesses()
方法返回對此程序可見的所有程序的快照。 不保證流在流處理時包含作業系統中的所有程序。
獲取快照後,程序可能已被終止或建立。 以下程式碼段列印按其PID排序的所有程序的PID:
System.out.printf("All processes PIDs:%n");
ProcessHandle.allProcesses()
.map(ph -> ph.getPid())
.sorted()
.forEach(System.out::println);
可以為所有執行的程序計算不同型別的統計資訊。 還可以在Java中建立一個工作管理員,顯示一個UI,顯示所有正在執行的程序及其屬性。 下面程式碼顯示瞭如何獲得執行時間最長的程序細節以及最多使用CPU時間的程序。 比較了程序的開始時間,以獲得最長的執行程序和總CPU持續時間,以獲得使用CPU時間最多的程序。 你可能會得到不同的輸出。 程式碼在Windows 10上執行程式時,得到了這個輸出。
// ProcessStats.java
package com.jdojo.process.api;
import java.time.Duration;
import java.time.Instant;
public class ProcessStats {
public static void main(String[] args) {
System.out.printf("Longest CPU User Process:%n");
ProcessHandle.allProcesses()
.max(ProcessStats::compareCpuTime)
.ifPresent(CurrentProcessInfo::printInfo);
System.out.printf("%nLongest Running Process:%n");
ProcessHandle.allProcesses()
.max(ProcessStats::compareStartTime)
.ifPresent(CurrentProcessInfo::printInfo);
}
public static int compareCpuTime(ProcessHandle ph1,
ProcessHandle ph2) {
return ph1.info()
.totalCpuDuration()
.orElse(Duration.ZERO)
.compareTo(ph2.info()
.totalCpuDuration()
.orElse(Duration.ZERO));
}
public static int compareStartTime(ProcessHandle ph1,
ProcessHandle ph2) {
return ph1.info()
.startInstant()
.orElse(Instant.now())
.compareTo(ph2.info()
.startInstant()
.orElse(Instant.now()));
}
}
輸出結果為:
Longest CPU User Process:
PID: 10696
IsAlive: true
Command: C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
Arguments: []
CommandLine:
Start Time: 2016-11-28T10:12:08.537-06:00[America/Chicago]
CPU Time: PT14M26.5S
Owner: kishori\ksharan
Children Count: 0
Longest Running Process:
PID: 0
IsAlive: false
Command:
Arguments: []
CommandLine:
Start Time: 2016-11-29T13:18:22.262776600-06:00[America/Chicago]
CPU Time: PT0S
Owner: Unknown
Children Count: 127
七. 終止程序
可以使用ProcessHandle
介面和Process
類的destroy()
或destroyForcibly()
方法終止程序。 如果終止程序的請求成功,則兩個方法都返回true,否則返回false。 destroy()
方法請求正常終止,而destroyForcibly()
方法請求強制終止。 在執行終止程序的請求後,isAlive()
方法可以在短時間內返回true。
Tips
無法終止當前程序。 呼叫當前程序中的destroy()
或destroyForcibly()
方法會引發IllegalStateException
異常。 作業系統訪問控制可能會阻止程序終止。
一個程序的正常終止讓程序徹底終止。 強制終止流程將立即終止流程。 程序是否正常終止是依賴於實現的。 可以使用ProcessHandle
介面的supportsNormalTermination()
方法和Process
類來檢查程序是否支援正常終止。 如果程序支援正常終止,該方法返回true,否則返回false。
呼叫這些方法來終止已經被終止的程序導致沒有任何操作。 當程序結束後,Process
類的onExit()
返CompletableFuture<Process>
,ProcessHandle
介面的onExit()
方法返回CompletableFuture<ProcessHandle>
。
八. 管理程序許可權
執行上一節中的示例時,認為沒有安裝Java安全管理器。 如果安裝了安全管理器,則需要授予適當的許可權才能啟動,管理和查詢本地程序:
- 如果要建立新程序,則需要具有
FilePermission(cmd,"execute")
許可權,其中cmd
是將建立程序的命令的絕對路徑。 如果cmd
不是絕對路徑,則需要具有FilePermission("<<ALL FILES>>","execute")
許可權。 - 使用
ProcessHandle
介面中的方法來查詢本地程序的狀態並銷燬程序,應用程式需要具有RuntimePermission("manageProcess")
許可權。
下面包含一個獲取程序計數並建立新程序的程式。 它重複這兩個任務,一個任務沒有安全管理員許可權,而另一個任務有安全管理員許可權。
// ManageProcessPermission.java
package com.jdojo.process.api;
import java.util.concurrent.ExecutionException;
public class ManageProcessPermission {
public static void main(String[] args) {
// Get the process count
long count = ProcessHandle.allProcesses().count();
System.out.printf("Process Count: %d%n", count);
// Start a new process
Process p = Job.startProcess(1, 3);
try {
p.toHandle().onExit().get();
} catch (InterruptedException | ExecutionException e) {
System.out.println(e.getMessage());
}
// Install a security manager
SecurityManager sm = System.getSecurityManager();
if(sm == null) {
System.setSecurityManager(new SecurityManager());
System.out.println("A security manager is installed.");
}
// Get the process count
try {
count = ProcessHandle.allProcesses().count();
System.out.printf("Process Count: %d%n", count);
} catch(RuntimeException e) {
System.out.println("Could not get a " +
"process count: " + e.getMessage());
}
// Start a new process
try {
p = Job.startProcess(1, 3);
p.toHandle().onExit().get();
} catch (InterruptedException | ExecutionException |
RuntimeException e) {
System.out.println("Could not start a new " +
"process: " + e.getMessage());
}
}
}
假設沒有更改任何Java策略檔案,請嘗試使用以下命令執行ManageProcessPermission
類:
C:\Java9Revealed>java --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.ManageProcessPermission
輸出結果為:
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=6320) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
Job (pid=6320) is going to sleep for 1 seconds.
A security manager is installed.
Could not get a process count: access denied ("java.lang.RuntimePermission" "manageProcess")
Could not start a new process: access denied ("java.lang.RuntimePermission" "manageProcess")
你可能會得到不同的輸出。 輸出表示可以在安裝安全管理器之前獲取程序計數並建立新程序。 安裝安全管理器後,Java執行時會在請求程序計數和建立新程序時丟擲異常。 要解決此問題,需要授予以下四個許可權:
- “manageProcess” 執行時許可權,它將允許應用程式查詢本地程序並建立一個新程序。
- 在Java命令路徑上“execute” 檔案許可權,這將允許啟動JVM。
- 在系統屬性“jdk.module.path”和“java.class.path”中“read”的屬性許可權,因此在建立命令列以啟動JVM時,
Job
類可以讀取這些屬性。
如下包含一個指令碼,將這四個許可權授予所有程式碼。 需要將此指令碼新增到計算機上的JDK_HOME\conf\security\java.policy檔案中。 Java啟動器的路徑是C:\java9\bin\java.exe,只有在C:\java9目錄中安裝了JDK 9,才在Windows上有效。 對於所有其他平臺和JDK安裝,請修改此路徑以指向計算機上正確的Java啟動器。
grant {
permission java.lang.RuntimePermission "manageProcess";
permission java.io.FilePermission "C:\\java9\\bin\\java.exe", "execute";
permission java.util.PropertyPermission "jdk.module.path", "read";
permission java.util.PropertyPermission "java.class.path", "read";
};
如果使用相同的命令再次執行ManageProcessPermission
類,則應該獲得類似於以下內容的輸出:
Process Count: 133
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=3108) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
Job (pid=3108) is going to sleep for 1 seconds.
A security manager is installed.
Process Count: 133
Command used:
C:\java9\bin\java.exe --module-path
C:\Java9Revealed\com.jdojo.process.api\build\classes --module
com.jdojo.process.api/com.jdojo.process.api.Job 1 3
Job (pid=3684) info: Sleep Interval=1 seconds, Sleep Duration=3 seconds.
Job (pid=3684) is going to sleep for 1 seconds.
Job (pid=3684) is going to sleep for 1 seconds .
Job (pid=3684) is going to sleep for 1 seconds.
九. 總結
Process API由使用本地程序的類和介面組成。 Java SE從版本1.0通過執行時和程序類提供了Process API。 它允許建立新的本地程序,管理其I/O流並銷燬它們。 Java SE的更新版本改進了API。 直到Java 9,開發人員必須訴諸編寫原生代碼來獲取基本資訊,例如程序的ID,用於啟動程序的命令等。Java 9添加了一個名為ProcessHandle
的介面,表示程序控制代碼。 可以使用程序控制代碼來查詢和管理本地程序。
以下類和介面組成了Process API:Runtime
,ProcessBuilder
,ProcessBuilder.Redirect
,Process
,ProcessHandle
和ProcessHandle.Info
。
Runtime
類的exec()
方法用於啟動本地程序。
ProcessBuilder
類的start()
方法是優先於Runtime
類的exec()
方法來啟動程序。 ProcessBuilder.Redirect
類的例項表示程序的程序輸入源或程序的目標輸出。
Process
類的例項表示由Java程式建立的本地程序。
ProcessHandle
介面的例項表示由Java程式或其他方式建立的程序。它在Java 9中新增,並提供了幾種方法來查詢和管理程序。 ProcessHandle.Info
介面的例項表示程序的快照資訊; 它可以使用Process
類或ProcessHandle
介面的info()
方法獲得。 如果有一個程序例項,使用它的toHandle()
方法獲得一個ProcessHandle
。
ProcessHandle
介面的onExit()
方法返回一個用於終止程序的CompletableFuture<ProcessHandle>
。 可以使用返回的物件來新增在程序終止時執行的任務。 請注意,不能在當前程序中使用此方法。
如果安裝了一個安全管理器,則應用程式需要有一個“manageProcess”執行時許可權來查詢和管理本地程序,並在Java程式碼啟動的程序的命令檔案上“execute” 檔案許可權。