1. 程式人生 > >Java 9 揭祕(12. Process API 更新)

Java 9 揭祕(12. Process API 更新)

Tips
做一個終身學習的人。

Java 9

在本章中,主要介紹以下內容:

  • 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中有一些改進,就是在ProcessProcessBuilder類中新增幾個方法。

在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類包含一個返回ProcessHandletoHandle()方法。

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() 返回程序的使用者。

現在是時候看到ProcessHandleProcessHandle.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:RuntimeProcessBuilderProcessBuilder.RedirectProcessProcessHandleProcessHandle.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” 檔案許可權。