Java17 教程·续(八)
原文:More Java 17
十一、进程 API
在本章中,您将学习:
-
什么是进程 API
-
如何与运行 Java 应用程序的当前进程交互
-
如何创建本机进程
-
如何获取有关新进程的信息
-
如何获取当前进程的信息
-
如何获取所有系统进程的信息
-
如何设置创建、查询和管理本机进程的权限
本章中的所有示例程序都是清单 11-1 中声明的jdojo.process模块的成员。
// module-info.java
module jdojo.process {
exports com.jdojo.process;
}
Listing 11-1The Declaration of a jdojo.process Module
什么是进程 API?
Process API 由允许您在 Java 程序中使用本地进程的类和接口组成。使用 API,您可以
-
从 Java 代码创建新的本地进程。
-
获取本机进程的进程句柄,无论它们是由 Java 代码还是其他方式创建的。
-
销毁正在运行的本机进程。
-
查询进程的活性及其其他属性。
-
获取进程的子进程和父进程的列表。
-
获取本机进程的进程 ID (PID)。
-
获取新创建进程的输入、输出和错误流。
-
等待进程终止。
-
当进程终止时执行任务。
进程 API 很小。它由表 11-1 中列出的类和接口组成。我将在接下来的章节中用例子详细解释这些类和接口。
表 11-1
进程 API 的类和接口
|类别/接口
|
描述
|
| --- | --- |
| Runtime | 它是一个单例类,其唯一的实例代表了 Java 应用程序的运行时环境。 |
| ProcessBuilder | ProcessBuilder类的一个实例保存了一组进程的属性。调用它的start()方法启动一个本地进程,并返回一个代表本地进程的Process类的实例。可以多次调用它的start()方法;每次,它都使用保存在ProcessBuilder实例中的属性启动一个新的进程。 |
| ProcessBuilder.Redirect | 它是一个静态嵌套类,表示进程输入的源或进程输出的目的。 |
| Process | 它是一个抽象类,其实例表示当前 Java 程序使用ProcessBuilder的start()方法或Runtime的exec()方法启动的本地进程。 |
| ProcessHandle | 它是一个接口,其实例表示本地进程的句柄,无论这些进程是由当前 Java 程序还是由任何其他方式启动的。您可以使用此句柄控制和查询本机进程的状态。 |
| ProcessHandle.Info | ProcessHandle.Info接口的一个实例表示一个进程属性的快照。 |
在 Java 中,您可以启动本地进程,并使用它们的输入、输出和错误流。此外,还可以使用未启动的本地进程,并查询进程的详细信息。对于后者,您使用进程 API 内部的一个名为ProcessHandle的接口。接口的一个实例标识了一个本地进程;它允许您查询进程状态和管理进程。
比较一下Process类和ProcessHandle接口。Process类的实例表示由当前 Java 程序启动的本地进程,而ProcessHandle接口的实例表示由当前 Java 程序或其他方式启动的本地进程。Process类包含一个返回ProcessHandle的toHandle()方法。
ProcessHandle.Info接口的一个实例表示一个进程属性的快照。请注意,不同的操作系统实现的进程不同,因此它们的属性也不同。进程的状态可能随时改变,例如,每当进程获得更多的 CPU 时间时,进程使用的 CPU 时间就会增加。为了获得进程的最新信息,您需要在需要的时候使用ProcessHandle接口的info()方法,这将返回一个ProcessHandle.Info接口的新实例。
本章中的所有例子都是在 Ubuntu Linux 上运行的。当您在使用 Windows 或任何其他不同操作系统的计算机上运行这些程序时,您可能会得到不同的输出。
Note
通过调整可执行文件和参数文件路径,CLI 代码片段可以很容易地转换为 Windows 代码片段。
了解运行时环境
每个 Java 应用程序都有一个Runtime类的实例,它允许您查询当前 Java 应用程序运行的运行时环境并与之交互。Runtime类是单例的。您可以使用该类的getRuntime()静态方法获得它的唯一实例:
// Get the instance of the Runtime
Runtime runtime = Runtime.getRuntime();
使用Runtime,可以知道当前 JVM 可以使用的最大内存,JVM 中当前分配的内存,以及 JVM 中的空闲内存。这里有三种方法可以让您以字节为单位查询 JVM 的内存:
-
long maxMemory() -
long totalMemory() -
long freeMemory()
JVM 延迟分配内存。maxMemory()方法返回 JVM 可以分配的最大内存量。如果没有最大内存限制,该方法返回Long.MAX_VALUE。
totalMemory()方法返回 JVM 当前分配的最大内存。当 JVM 需要更多内存时,它会分配更多内存,而totalMemory()方法将返回当前分配的内存。JVM 可以分配最大内存,最大为由maxMemory()方法返回的内存量。
freeMemory()方法从 JVM 当前分配的内存中返回未使用的内存。你如何知道 JVM 使用的内存?下面的公式将给出 JVM 在特定时间点使用的内存:
Used Memory = Total Memory Free Memory
使用availableProcessors()方法获得 JVM 的可用处理器数量。
使用version()方法获得一个代表 Java 运行时环境版本的Runtime.Version。关于 JDK/JRE 版本化方案的更多细节,请参考Runtime.Version类的 Javadoc。清单 11-2 向您展示了Runtime类在查询 Java 运行时环境中的一些应用。您可能会得到不同的输出。
// QueryingRuntime.java
package com.jdojo.process;
public class QueryingRuntime {
public static void main(String[] args) {
// Get the Runtime instance
Runtime rt = Runtime.getRuntime();
// Get the JVM memory
long maxMemory = rt.maxMemory();
long totalMemory = rt.totalMemory();
long freeMemory = rt.freeMemory();
long usedMemory = totalMemory freeMemory;
System.out.format(
"Max memory = %d, Total memory = %d,"
+ "Free memory = %d, Used memory = %d.%n",
maxMemory, totalMemory, freeMemory,
usedMemory);
// Print the number of processors available to
// the JVM
int processors = rt.availableProcessors();
System.out.format("Number of processors = %d%n",
processors);
// Print the version of the Java runtime
Runtime.Version version = rt.version();
System.out.format("Version = %s%n",
version);
}
}
Max memory = 3126853632,
Total memory = 201326592,
Free memory = 198351728,
Used memory = 2974864.
Number of processors = 8
Version = 17+01-123
Listing 11-2Querying the Java Runtime Environment
您可以使用Runtime类的gc()方法调用垃圾收集。System.gc()静态方法是Runtime.getRuntime().gc()的方便方法。
Note
方法gc()只是提示操作系统在下一个方便的时间段开始垃圾收集。如果gc()被调用,你不能依赖垃圾收集来立即开始。
您可以使用Runtime类的exit(int status)方法终止 JVM。System.exit()静态方法是Runtime.getRuntime().exit()的一个方便方法。按照惯例,status的非零值表示 JVM 的异常终止。您可以使用Runtime类的halt()方法强制终止 JVM。
您可以使用Runtime类的addShutdownHook(Thread hook)和removeShutdownHook(Thread hook)方法添加和移除 JVM 的关闭挂钩。关闭挂钩是一个线程,它被初始化,但没有启动。当线程终止时,JVM 启动注册为关闭挂钩的线程。
使用它的一个exec()重载方法来启动一个本地进程。您应该使用ProcessBuilder类来启动一个本地进程。Runtime类的exec()方法在内部使用了ProcessBuilder类。
当前进程
ProcessHandle接口的current()静态方法返回当前进程的句柄。请注意,此方法返回的当前进程始终是执行代码的 Java 进程:
// Get the handle of the current process
ProcessHandle current = ProcessHandle.current();
一旦获得了当前进程的句柄,就可以使用ProcessHandle接口的方法来获得关于该进程的细节。请参考下一节中关于如何获取当前进程信息的示例。
Note
您不能终止当前进程。试图使用ProcessHandle接口的destroy()或destroyForcibly()方法终止当前进程会导致IllegalStateException。
查询进程状态
您可以使用ProcessHandle接口中的方法来查询进程的状态。表 11-2 列出了该接口的常用方法,并做了简要说明。请注意,这些方法中有许多会返回快照时为真的进程状态的快照。因为进程是异步创建、运行和销毁的,所以当您以后使用它的属性时,不能保证进程仍然处于相同的状态。
表 11-2
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。终止一个进程会立即强制终止该进程,而正常终止则允许进程干净地关闭。能否终止一个进程取决于操作系统的访问控制。 |
| ProcessHandle.Info info() | 返回进程信息的快照。 |
| boolean isAlive() | 如果此ProcessHandle所代表的进程尚未终止,则返回true,否则返回false。请注意,在您成功请求终止进程后的一段时间内,该方法可能会返回true,因为进程将被异步终止。 |
| static Optional<ProcessHandle> of(long pid) | 为现有的本地进程返回一个Optional<ProcessHandle>。如果具有指定的pid的进程不存在,则返回空的Optional。 |
| CompletableFuture <ProcessHandle> onExit() | 返回一个CompletableFuture <ProcessHandle>来终止进程。您可以使用返回的对象添加一个任务,该任务将在进程终止时执行。在当前进程上调用这个方法会抛出一个IllegalStateException。 |
| Optional<ProcessHandle> parent() | 为父进程返回一个Optional<ProcessHandle>。 |
| long pid() | 返回由操作系统分配的进程的本机进程 ID (PID)。注意,如果进程终止,PID 可以被操作系统重用,因此具有相同 PID 的两个进程句柄可能不代表相同的进程。 |
| boolean supportsNormalTermination() | 如果destroy()的执行正常终止进程,则返回true。 |
表 11-3 列出了ProcessHandle.Info嵌套接口的方法和描述。此接口的实例包含有关进程的快照信息。你可以使用ProcessHandle接口的info()方法或者Process类获得一个ProcessHandle.Info。接口中的所有方法都返回一个Optional。
表 11-3
方法。信息界面
|方法
|
描述
|
| --- | --- |
| Optional<String[]> arguments() | 返回进程的参数。该进程可能会在启动后更改传递给它的原始参数。在这种情况下,此方法返回更改的参数。 |
| Optional<String> command() | 返回进程的可执行路径名。 |
| Optional<String> commandLine() | 这是一种将进程的命令和参数结合起来的便捷方法。如果两个方法都返回非空选项,它通过组合从command()和arguments()方法返回的值来返回进程的命令行。 |
| Optional<Instant> startInstant() | 返回进程的开始时间。如果操作系统没有返回开始时间,它返回一个空的Optional。 |
| Optional<Duration> totalCpuDuration() | 返回进程使用的总 CPU 时间。请注意,一个进程可能会运行很长时间,并且可能会占用很少的 CPU 时间。 |
| Optional<String> user() | 返回进程的用户。 |
是时候看看ProcessHandle和ProcessHandle.Info接口的作用了。清单 11-3 包含一个名为CurrentProcessInfo的类的代码。它的printInfo()方法以一个ProcessHandle作为参数,并打印进程的细节。我们还在其他例子中使用这种方法来打印进程的细节。main()方法获取运行该进程的当前进程的句柄,该进程是一个 Java 进程,并打印其详细信息。您可能会得到不同的输出。当程序在 Linux 上运行时,会生成输出。
// CurrentProcessInfo.java
package com.jdojo.process;
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.pid();
// 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: 4143
IsAlive: true
Command: /opt/jdk17/bin/java
Arguments: [-Dfile.encoding=UTF-8,
-classpath,
[<path-to-project>]/bin,
-XX:+ShowCodeDetailsInExceptionMessages,
com.jdojo.process.CurrentProcessInfo]
CommandLine: /opt/openjdk-16.36/bin/java
-Dfile.encoding=UTF-8
-classpath [<path-to-project>]/bin
-XX:+ShowCodeDetailsInExceptionMessages
com.jdojo.process.CurrentProcessInfo
Start Time: 2021-07-16T14:50:18.870+02:00
[Europe/Berlin]
CPU Time: PT0.06S
Owner: peter
Children Count: 0
Listing 11-3A CurrentProcessInfo Class That Prints the Details of the Current Process
比较进程
比较两个进程的相等性或有序性是很棘手的。您不能依赖 PID 来实现进程的平等。进程终止后,操作系统重用 PID。您可以检查进程的开始时间以及 PIDs 如果相同,则两个进程可能相同。ProcessHandle接口的默认实现的equals()方法检查两个进程的以下三条信息是否相等:
-
两个进程的
ProcessHandle接口的实现类必须相同。 -
进程必须有相同的 PID。
-
进程必须同时启动。
Note
在ProcessHandle接口中使用compareTo()方法的默认实现对于排序不是很有用。它比较了两个进程的 PID。
创建进程
您需要使用ProcessBuilder类的一个实例来启动一个新的本地进程。一个ProcessBuilder管理本地进程属性的集合。一旦为进程设置了所有属性,就可以调用它的start()方法来启动一个新的本地进程。存储在ProcessBuilder中的属性将用于启动新进程。您可以多次调用start()方法,使用存储在ProcessBuilder中的属性启动新的进程。start()方法返回代表新的本地进程的Process类的实例。您可以使用以下构造函数之一来创建ProcessBuilder类的实例:
-
ProcessBuilder(String... command) -
ProcessBuilder(List<String> command)
构造函数允许您指定操作系统程序和参数。假设您想在 Linux 上从/opt/jdk17/bin内部运行java程序,如下所示:
/opt/jdk17/bin/java --version
您将创建一个ProcessBuilder来表示这个命令,如下所示:
ProcessBuilder pb = new ProcessBuilder(
"/opt/jdk17/bin/java", "--version");
使用ProcessBuilder类的方法,您可以管理进程的以下属性:
-
一个命令
-
一个环境
-
工作目录
-
标准输入/输出(
stdin、stdout和stderr) -
标准错误流的重定向属性
命令只是代表外部程序及其参数的字符串列表。可以在ProcessBuilder类的构造函数中设置命令。以下方法允许您检索命令字符串并设置更多命令字符串:
-
List<String> command() -
ProcessBuilder command(String... command)
不带任何参数的command()方法返回已经在ProcessBuilder中设置的命令字符串。带有 varargs 参数的command()方法允许您添加更多的命令字符串。下面的代码片段创建了一个ProcessBuilder来在 Linux 上启动 JVM。它使用command()方法来设置命令属性:
ProcessBuilder pb = new ProcessBuilder()
.command("/opt/jdk17/bin/java",
"--module-path",
"myModulePath",
"--module",
"myModule/className");
环境是依赖于系统的键值对的列表。它被初始化为从静态方法System.getEnv()返回的Map<String,String>的副本。您需要使用ProcessBuilder类的environment()方法来获取Map<String,String>并将键值对添加到映射中。下面的代码片段向您展示了如何为ProcessBuilder设置环境属性:
ProcessBuilder pb = new ProcessBuilder("mycommand");
Map<String,String> env = pb.environment();
env.put("arg1", "value1");
env.put("arg2", "value2");
默认情况下,新进程的工作目录是当前 Java 进程的工作目录,通常是由系统属性user.dir命名的目录。ProcessBuilder类中的以下方法允许您获取和设置工作目录:
-
File directory() -
ProcessBuilder directory(File directory)
下面的代码片段向您展示了如何在 Linux 上将工作目录设置为/home/USER/mydir:
ProcessBuilder pb = new ProcessBuilder("myCommand")
.directory(new File("/home/USER/mydir"));
由ProcessBuilder的start()方法创建的新进程被创建为当前进程的子进程,当前进程是运行代码的 Java 进程。换句话说,当前的 Java 进程是新创建的进程的父进程。新进程不拥有标准 I/O ( stdin、stdout和stderr)的终端或控制台。默认情况下,新进程的 I/O 通过管道连接到父进程。您可以选择通过调用一个ProcessBuilder的inheritIO()方法将新进程的标准 I/O 设置为与其父进程相同。在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 时间等。您获得的关于进程的信息以及您对进程的控制取决于操作系统的访问控制。
很难拿出例子来创建可以在所有操作系统上运行的进程。如果您可以运行本书中的其他示例,这意味着您的机器上安装了 JDK17。您可以使用机器上的java程序来启动示例中的其他进程。您可以使用当前进程的 command 属性,即当前运行的java程序,来获取 Java 程序在您的机器上的路径,因此这些示例将在所有平台上工作。
让我们看几个使用 Java 程序创建本地进程的例子。您可以分别使用–version和-version选项将 Java 产品版本信息打印到标准输出和标准错误中,如下所示:
/opt/jdk17/bin/java --version
openjdk 17 2021-05-16
OpenJDK Runtime Environment (build 17+1-123)
OpenJDK 64-Bit Server VM (build 17+1-123, mixed mode, sharing)
/opt/jdk17/bin/java -version
openjdk 17 2021-05-16
OpenJDK Runtime Environment (build 17+1-123)
OpenJDK 64-Bit Server VM (build 17+1-123, mixed mode, sharing)
在前面的输出中,您看不到输出在哪里打印的任何区别。两个输出都打印到同一个控制台,因为默认情况下,标准输出和标准错误都映射到控制台。但是,当您尝试在程序中捕获这两个命令的输出时,您会看到不同之处。
清单 11-4 显示了一个运行java –version命令将 Java 产品信息打印到标准输出的程序。
// PipedIO.java
package com.jdojo.process;
import java.io.IOException;
public class PipedIO {
public static void main(String[] args) {
// Get the path of the java program that started
// this program
String javaPath = ProcessHandle.current()
.info()
.command().orElse(null);
if(javaPath == null) {
System.out.println(
"Could not get the java command's path.");
return;
}
// Configure the ProcessBuilder
ProcessBuilder pb =
new ProcessBuilder(javaPath, "--version");
try {
// Start a new java process
Process p = pb.start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Listing 11-4Capturing the Output of a Native Process
当你运行程序ProcessIO类时,它不打印任何东西。产出去了哪里?程序创建了一个新进程,该进程的标准输出通过管道连接到父进程。如果您想要访问输出,您需要从适当的管道读取。当新进程的标准 I/O 通过管道传输到父进程时,您可以使用Process的以下方法来获取新进程的 I/O 流:
-
OutputStream getOutputStream() -
InputStream getInputStream() -
InputStream getErrorStream()
从getOutputStream()方法返回的OutputStream被连接到新进程的标准输入流。写入此输出流将通过管道传输到新进程的标准输入。
从getInputStream()返回的InputStream连接到新进程的标准输出。如果您想要捕获新进程的标准输出,您需要从这个输入流中读取。
从getErrorStream()返回的InputStream连接到新进程的标准误差。如果您想要捕获新进程的标准错误,您需要从这个输入流中读取。有时,您希望将输出合并到标准输出,并将标准错误合并到一个目的地。它给出了输出和错误的准确顺序,以便于解决问题。您可以调用ProcessBuilder的redirectErrorStream(true)方法,将写入标准错误的数据发送到标准输出。我很快会展示这类例子。
Note
您可以选择将新进程的标准 I/O 重定向到其他目的地,比如文件,在这种情况下,getOutputStream()、getInputStream()和getErrorStream()方法返回null。
清单 11-5 中的程序修复了在PipedIO类中得不到任何输出的问题。它读取并打印写入管道中标准输出流的数据。
// CapturePipedIO.java
package com.jdojo.process;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class CapturePipedIO {
public static void main(String[] args) {
// Get the path of the java program that started
// this program
String javaPath = ProcessHandle.current()
.info()
.command().orElse(null);
if (javaPath == null) {
System.out.println(
"Could not get the java command's path.");
return;
}
// Configure the ProcessBuilder
ProcessBuilder pb =
new ProcessBuilder(javaPath, "--version");
try {
// Start a new java process
Process p = pb.start();
// Read and print the standard output stream
// of the process
try (BufferedReader input =
new BufferedReader(
new InputStreamReader(
p.getInputStream()))) {
String line;
while ((line = input.readLine()) != null) {
System.out.println(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
openjdk 17 2021-05-16
OpenJDK Runtime Environment (build 17+1-123)
OpenJDK 64-Bit Server VM (build 17+1-123, mixed mode, sharing)
Listing 11-5Capturing the Output of a Native Process
如果运行带有-version选项的java命令,输出将被写入标准错误。如果您将清单 11-5 中的选项从–version更改为-version,您将不会再次获得任何输出,因为输出将通过管道传输到标准错误流。有两种方法可以解决这个问题:
-
在程序中,从
Process的getErrorStream()方法返回的InputStream读取,而不是从getInputStream()方法返回的InputStream读取。 -
将错误流重定向到标准输出流,并继续从标准输出中读取。
以下代码片段使用java -version命令创建了一个ProcessBuilder,并在标准输出中重定向了错误流:
// Configure the ProcessBuilder
ProcessBuilder pb =
new ProcessBuilder(javaPath, "-version")
.redirectErrorStream(true);
如果您将创建清单 11-5 中的ProcessBuilder的语句改为这个语句,您的程序将运行良好。
新进程也可以继承父进程的标准 I/O。如果要将新进程的所有 I/O 目的地设置为与当前进程相同,请使用ProcessBuilder的inheritIO()方法,如下所示:
// Configure the ProcessBuilder inheriting parent's I/O
ProcessBuilder pb =
new ProcessBuilder(javaPath, "--version")
.inheritIO();
如果您更改清单 11-4 中的代码以匹配前面的代码片段,您将看到输出。
ProcessBuilder.Redirect嵌套类表示由ProcessBuilder创建的新进程的输入源和输出目的地。该类定义了以下三个ProcessBuilder .Redirect类型的常量:
-
ProcessBuilder.Redirect DISCARD:丢弃新进程的输出 -
ProcessBuilder.Redirect.INHERIT:表示新进程的输入源或输出目的地将与当前进程相同 -
ProcessBuilder.Redirect.PIPE:表示新进程将通过管道连接到当前进程,这是默认设置
您还可以使用Process.Redirect类的以下方法将新进程的输入和输出重定向到一个文件:
-
ProcessBuilder.Redirect appendTo(File file) -
ProcessBuilder.Redirect from(File file) -
ProcessBuilder.Redirect to(File file)
在前面的代码片段中,您看到了如何使用ProcessBuilder类的inheritIO()方法让新进程拥有与当前进程相同的标准 I/O。您可以按如下方式重写代码:
// Configure the ProcessBuilder inheriting parent's I/O
ProcessBuilder pb =
new ProcessBuilder(javaPath, "--version")
.redirectInput(ProcessBuilder.Redirect.INHERIT)
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT);
下面的代码片段将新进程的标准输出重定向到当前目录中名为java_product_details.txt的文件:
// Configure the ProcessBuilder
ProcessBuilder pb =
new ProcessBuilder(javaPath, "--version")
.redirectOutput(
ProcessBuilder.Redirect.to(
new File("java_product_details.txt")));
让我们看一个复杂的例子,它将探索关于新的本地进程的更多信息。清单 11-6 包含一个名为Job的类的代码。它的main()方法需要两个参数:睡眠间隔和以秒为单位的睡眠持续时间。如果没有通过,该方法将使用 5 秒和 60 秒作为默认值。在第一部分,该方法尝试提取第一个和第二个参数(如果指定)。在第二部分中,它使用 ProcessHandle.current()方法获取执行该方法的当前进程的进程句柄。它读取当前进程的 PID,并打印一条包括 PID、睡眠间隔和睡眠持续时间的消息。最后,它开始一个 for 循环,并在睡眠间隔内保持睡眠,直到达到睡眠持续时间。在循环的每次迭代中,它都会打印一条消息。
// Job.java
package com.jdojo.process;
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().pid();
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);
}
}
}
Listing 11-6The Declaration of a Class Named Job
Job类包含一个启动新进程的startProcess(long sleepInterval, long sleepDuration)方法。它启动一个以Job类为主类的 JVM。它将休眠间隔和持续时间作为参数传递给 JVM。该方法试图构建一个命令来从JDK_HOME\bin目录启动java命令。如果Job类被加载到一个命名的模块中,它将构建一个类似这样的命令:
JDK_HOME/bin/java --module-path <module-path> \
--module jdojo.process/com.jdojo.process.Job \
<sleepInterval> <sleepDuration>
如果Job类被加载到一个未命名的模块中,它会尝试构建一个如下所示的命令:
JDK_HOME/bin/java \
-class-path <class-path> \
com.jdojo.process.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);
您可以使用您在清单 11-3 中创建的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.pid());
});
您可以等待新进程终止,如下所示:
// Wait for the process to terminate
future.get();
在这个例子中,future.get()将返回进程的ProcessHandle。我没有使用返回值,因为我已经在handle变量中有了它。
清单 11-7 包含了一个StartProcessTest类的代码,展示了如何使用Job类创建一个新的进程。在它的main()方法中,它创建一个新进程,打印进程细节,向进程添加一个关闭任务,等待进程终止,然后再次打印进程细节。请注意,该进程运行了 15 秒,但它只使用了 0.359375 秒的 CPU 时间,因为该进程的主线程大部分时间都在休眠。您可能会得到不同的输出。当程序在 Linux 上运行时,会生成输出。
// StartProcessTest.java
package com.jdojo.process;
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.pid());
});
try {
// Wait for the process to complete
future.get();
} catch (InterruptedException
| ExecutionException e) {
e.printStackTrace();
}
// Print process details again
CurrentProcessInfo.printInfo(handle);
}
}
Command used:
/opt/jdk17/bin/java
--class-path /[<path-to-project>]/bin
com.jdojo.process.Job 5 15
PID: 8701
IsAlive: true
Command: /opt/jdk17/bin/java
Arguments: [
--class-path,
/[<path-to-project>]/bin,
com.jdojo.process.Job,
5, 15 ]
CommandLine: /opt/jdk17/bin/java
--class-path /[<path-to-project>]/bin
com.jdojo.process.Job
5 15
Start Time: 2021-07-16T18:11:42.510+02:00
[Europe/Berlin]
CPU Time: PT0.01S
Owner: peter
Children Count: 0
Job (pid=8701) info:
Sleep Interval=5 seconds, Sleep Duration=15 seconds.
Job (pid=8701) is going to sleep for 5 seconds.
Job (pid=8701) is going to sleep for 5 seconds.
Job (pid=8701) is going to sleep for 5 seconds.
Job (pid=8701) terminated.
PID: 8701
IsAlive: false
Command:
Arguments: []
CommandLine:
Start Time: 2021-07-16T18:11:58.489975569+02:00
[Europe/Berlin]
CPU Time: PT0S
Owner: Unknown
Children Count: 0
Listing 11-7A StartProcessTest Class That Creates New Processes
获取进程句柄
有几种方法可以获得本机进程的句柄。对于由 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 进程。你已经在清单 11-3 中看到了这样的例子。
parent()方法返回父进程的句柄。如果该进程没有父进程或者无法检索父进程,它将返回一个空的Optional。
children()方法返回该进程所有直接子进程的快照。不能保证此方法返回的进程仍处于活动状态。请注意,不活跃的进程没有子进程。
descendants()方法返回该进程所有直接或间接子进程的快照。
allProcesses()方法返回该进程可见的所有进程的快照。不能保证流在处理时包含操作系统中的所有进程。在拍摄快照后,可能已经终止或创建了进程。下面的代码片段打印了按 PID 排序的所有进程的 PID:
System.out.printf("All processes PIDs:%n");
ProcessHandle.allProcesses()
.map(ph -> ph.pid())
.sorted()
.forEach(System.out::println);
您可以为所有正在运行的进程计算不同类型的统计数据。您还可以在 Java 中创建一个任务管理器,它显示一个 UI,该 UI 显示所有正在运行的进程及其属性。清单 11-8 展示了如何获得运行时间最长的进程的详细信息以及使用 CPU 时间最多的进程。我比较了进程的开始时间以获得运行时间最长的进程,并比较了总的 CPU 持续时间以获得使用 CPU 时间最多的进程。您可能会得到不同的输出。当我在 Linux 上运行这个程序时,我得到了这个输出。
// ProcessStats.java
package com.jdojo.process;
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: 2323
IsAlive: true
Command: /usr/lib/tracker/tracker-miner-fs
Arguments: []
CommandLine: /usr/lib/tracker/tracker-miner-fs
Start Time: 2021-07-16T13:43:03.590+02:00[Europe/Berlin]
CPU Time: PT14M35.72S
Owner: peter
Children Count: 0
Longest Running Process:
PID: 9019
IsAlive: true
Command: /opt/openjdk-16.36/bin/java
Arguments: [
-Dfile.encoding=UTF-8,
-classpath,
[...],
-XX:+ShowCodeDetailsInExceptionMessages,
com.jdojo.process.ProcessStats]
CommandLine: /opt/jdk17/bin/java
-Dfile.encoding=UTF-8
-classpath [...]
-XX:+ShowCodeDetailsInExceptionMessages
com.jdojo.process.ProcessStats
Start Time: 2021-07-16T19:02:01.020+02:00[Europe/Berlin]
CPU Time: PT0.3S
Owner: peter
Children Count: 0
Listing 11-8Computing Process Statistics
终止进程
您可以使用ProcessHandle接口的destroy()或destroyForcibly()方法和Process类来终止一个进程。如果终止进程的请求成功,两个方法都返回true,否则返回false。destroy()方法请求正常终止,而destroyForcibly()方法请求强制终止。在发出终止进程的请求后,isAlive()方法可能会返回true一小段时间。
Note
您不能终止当前进程。在当前进程上调用destroy()或destroyForcibly()方法会抛出一个IllegalStateException。操作系统访问控制可以防止进程被终止。
进程的正常终止让进程干净地终止。进程的强制终止会立即终止该进程。进程是否正常终止取决于实现。你可以使用ProcessHandle接口的supportsNormalTermination()方法和Process类来检查一个进程是否支持正常终止。如果进程支持正常终止,该方法返回true,否则返回false。
调用这些方法之一来终止已经终止的进程不会导致任何操作。当进程终止时,Process类的onExit()返回的CompletableFuture<Process>和ProcessHandle接口的onExit()返回的CompletableFuture<ProcessHandle>为completed。
管理进程权限
当您运行前几节中的示例时,我假设没有安装 Java 安全管理器。如果安装了安全管理器,则需要授予适当的权限来启动、管理和查询本机进程:
-
如果您正在创建一个新的进程,您需要拥有
FilePermission(cmd,"execute")权限,其中cmd是将创建该进程的命令的绝对路径。如果cmd不是绝对路径,你需要有FilePermission("<<ALL FILES>>","execute")权限。 -
要查询本地进程的状态并使用
ProcessHandle接口中的方法销毁进程,应用程序需要拥有RuntimePermission("manageProcess")权限。
清单 11-9 包含一个获取进程计数并创建一个新进程的程序。它在没有安全管理器和有安全管理器的情况下重复这两个任务。
// ManageProcessPermission.java
package com.jdojo.process;
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());
}
}
}
Listing 11-9Managing Processes with a Security Manager
假设您没有更改任何 Java 策略文件,尝试使用以下命令运行ManageProcessPermission类:
/opt/jdk17/bin/java \
-Dfile.encoding=UTF-8 \
-classpath /[<path-to-project>]/bin \
-XX:+ShowCodeDetailsInExceptionMessages \
com.jdojo.process.ManageProcessPermission
Process Count: 332
Command used:
/opt/jd17/bin/java
--class-path [...] com.jdojo.process.Job 1 3
Job (pid=3858) info: Sleep Interval=1 seconds,
Sleep Duration=3 seconds.
Job (pid=3858) is going to sleep for 1 seconds.
Job (pid=3858) is going to sleep for 1 seconds.
Job (pid=3858) 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" RuntimePermission,它将允许应用程序查询本机进程并创建一个新进程 -
Java 命令路径上的
"execute" FilePermission,它将允许启动 JVM -
"jdk.module.path"上的"read" PropertyPermission和"java.class.path"系统属性,因此Job类可以在构建启动 JVM 的命令行时读取这些属性
清单 11-10 包含一个将这四种权限授予所有代码的脚本。您需要将这个脚本添加到您机器上的JDK_HOME/conf/security/java.policy文件中。Java launcher 的路径是/opt/jdk17/bin/java,在 Linux 上只有在/opt/jdk17目录下安装了 JDK17 才有效。对于所有其他平台和 JDK 安装,修改该路径以指向您机器上正确的 Java 启动器。
grant {
permission java.lang.RuntimePermission
"manageProcess";
permission java.io.FilePermission
"/opt/jdk17/bin/java", "execute";
permission java.util.PropertyPermission
"jdk.module.path", "read";
permission java.util.PropertyPermission
"java.class.path", "read";
};
Listing 11-10Addendum to the JDK_HOME/conf/security/java.policy File
如果使用相同的命令再次运行ManageProcessPermission类,应该会得到类似如下的输出:
/opt/jdk17/bin/java \
-Dfile.encoding=UTF-8 \
-classpath /[<path-to-project>]/bin \
-XX:+ShowCodeDetailsInExceptionMessages \
com.jdojo.process.ManageProcessPermission
Process Count: 330
Command used:
/opt/jdk17/bin/java
--class-path [...]
com.jdojo.process.Job 1 3
Job (pid=6093) info: Sleep Interval=1 seconds,
Sleep Duration=3 seconds.
Job (pid=6093) is going to sleep for 1 seconds.
Job (pid=6093) is going to sleep for 1 seconds.
Job (pid=6093) is going to sleep for 1 seconds.
A security manager is installed.
Process Count: 330
Command used:
/opt/jdk17/bin/java
--class-path [...]
com.jdojo.process.Job 1 3
Job (pid=6114) info: Sleep Interval=1 seconds,
Sleep Duration=3 seconds.
Job (pid=6114) is going to sleep for 1 seconds.
Job (pid=6114) is going to sleep for 1 seconds.
Job (pid=6114) is going to sleep for 1 seconds.
摘要
进程 API 由与本地进程一起工作的类和接口组成。Java SE 从 1.0 版本开始就通过Runtime和Process类提供了进程 API。它允许您创建新的本机进程,管理它们的 I/O 流,并销毁它们。Java SE 的更高版本改进了 API,增加了一个名为ProcessHandle的接口来表示进程句柄。您可以使用进程句柄来查询和管理本机进程。
以下类和接口组成了进程 API: Runtime、ProcessBuilder、ProcessBuilder.Redirect、Process、ProcessHandle和ProcessHandle.Info。
Runtime类的exec()方法用于启动一个本地进程。在启动一个进程时,ProcessBuilder类的start()方法比Runtime类的exec()方法更可取。ProcessBuilder.Redirect类的一个实例代表一个进程的输入源或一个进程的输出目的地。
默认情况下,新进程的标准 I/O 通过管道连接到当前进程。您需要读写与管道相关的流,以访问新进程的标准 I/O。您可以选择将新进程的标准 I/O 设置为与当前进程相同,或者将 I/O 重定向到其他源/目标,如文件。
Process类的一个实例代表一个由 Java 程序创建的本地进程。
ProcessHandle接口的一个实例代表一个由 Java 程序或其他方式创建的进程;它是在 Java 9 中添加的,提供了几种查询和管理进程的方法。ProcessHandle.Info接口的一个实例代表一个进程的快照信息;它可以通过使用Process类的info()方法或ProcessHandle接口获得。如果你有一个Process实例,使用它的toHandle()方法得到一个ProcessHandle。
ProcessHandle接口的onExit()方法返回一个CompletableFuture<ProcessHandle>来终止进程。您可以使用返回的对象添加一个任务,该任务将在进程终止时执行。请注意,您不能在当前进程中使用此方法。
如果安装了安全管理器,应用程序需要有一个"manageProcess" RuntimePermission来查询和管理本地进程,并在从 Java 代码启动的进程的命令文件上有一个"execute" FilePermission。
练习
练习 1
什么是进程 API?
练习 2
Runtime类的实例代表什么?
运动 3
如何获得Runtime类的实例?
演习 4
如何使用ProcessBuilder类?这个类的什么方法被用来启动一个新的本地进程?
锻炼 5
Process类的实例代表什么?
锻炼 6
ProcessHandle接口的实例代表什么?你如何从一个Process那里获得一个ProcessHandle?
锻炼 7
如何获得代表正在运行的 Java 程序的当前进程的句柄?
运动 8
ProcessHandle.Info接口的实例代表什么?
演习 9
由ProcessBuilder类的start()方法创建的新进程的默认标准 I/O 是什么?
运动 10
可以使用 Process API 终止当前的 Java 程序吗?
十二、打包模块
在本章中,您将学习:
-
打包 Java 模块的不同格式
-
对 JAR 格式的增强
-
多释放罐是什么
-
如何创建和使用多版本 jar
-
JMOD 格式是什么
-
如何使用
jmod工具处理 JMOD 文件 -
如何创建、提取和描述 JMOD 文件
-
如何列出 JMOD 文件的内容
-
如何在 JMOD 文件中记录模块的哈希以进行依赖验证
一个模块可以打包成不同的格式,用于三个阶段:编译时、链接时和运行时。并非所有阶段都支持所有格式。Java 支持以下格式来打包模块:
-
展开的目录
-
JAR 格式
-
JMOD 格式
-
图像格式
JDK9 之前支持展开目录和 JAR 格式。JAR 格式在 JDK9 中得到增强,以支持模块化 JAR 和多版本 JAR。JDK9 引入了两种新的模块打包格式:JMOD 格式和 JIMAGE 格式。在本章中,我将讨论 JAR 格式和 JMOD 格式的增强。第十三章详细介绍了 JIMAGE 格式以及jlink工具。
JAR 格式
在本书中,我们还没有谈到非模块化和模块化的 jar。然而,这两种变体都属于入门风格的书籍,所以如果需要更多关于标准或模块化 jar 的信息,我们要求读者查阅 Oracle 的 Java 文档和命令帮助(输入jar -h)。
在这一章中,我将介绍一个添加到 JAR 格式中的新特性,它被称为多版本 JAR。
什么是多释放罐?
作为一名经验丰富的 Java 开发人员,您必须使用过 Java 库/框架,如 Spring framework、Hibernate 等。你可能在用 Java 17,但是那些库可能还在用 Java 8。为什么库开发者不能使用最新版本来利用 JDK 的新特性?原因之一是并非所有图书馆用户都使用最新的 JDK。更新图书馆以使用新版 JDK 意味着迫使所有图书馆用户迁移到新版 JDK,这在实践中是不可能的。维护和发布针对不同 JDK 的库是打包代码时的另一个痛苦。通常,您会为不同的 JDK 找到一个单独的库 JAR。Java 通过向库开发人员提供一种特殊的打包库代码的方式解决了这个问题——使用一个 JAR 包含多个 JDK 的相同版本的库。这种震击器被称为多释放震击器。
一个多版本 JAR (MRJAR)包含一个用于多个 JDK 版本的相同版本的库(提供相同的 API)。也就是说,您可以拥有一个作为 MRJAR 的库,它将为 JDK8 和 JDK17 工作。MRJAR 中的代码将包含在 JDK8 和 JDK17 中编译的类文件。用 JDK17 编译的类可以利用 JDK9 和更高版本提供的 API,而用 JDK8 编译的类可以提供用 JDK8 编写的相同的库 API。
MRJAR 扩展了 JAR 已经存在的目录结构。JAR 包含一个根目录,它的所有内容都驻留在这个根目录中。它包含一个用于存储 JAR 元数据的META-INF目录。通常,JAR 包含一个包含其属性的META-INF/MANIFEST.MF文件。典型 JAR 中的条目如下所示:
- jar-root
- C1.class
- C2.class
- C3.class
- C4.class
- META-INF
- MANIFEST.MF
JAR 包含四个类文件和一个MANIFEST.MF文件。MRJAR 扩展了META-INF目录来存储特定于 JDK 版本的类。META-INF目录包含一个versions子目录,其中可能包含许多子目录——每个子目录的名称都与 JDK 主版本相同。例如,对于特定于 JDK17 的类,可能有META-INF/versions/17目录,对于特定于 JDK16 的类,可能有一个名为META-INF/versions/16的目录,等等。典型的 MRJAR 可能包含以下条目:
- jar-root
- C1.class
- C2.class
- C3.class
- C4.class
- META-INF
- MANIFEST.MF
- versions
- 16
- C2.class
- C5.class
- 17
- C1.class
- C2.class
- C6.class
如果在不支持 MRJAR 的环境中使用这个 Mr JAR,它将被视为一个普通的 JAR——将使用根目录中的内容,而忽略META-INF/versions/17和META-INF/versions/16中的所有其他内容。所以,如果这个 MRJAR 与 JDK8 一起使用,那么只会使用四个类:C1、C2、C3和C4。
当这个 MRJAR 在 JDK16 中使用时,有五个类在起作用:C1、C2、C3、C4和C5。将使用META-INF/versions/9目录中的C2类,而不是根目录中的C2类。在这种情况下,MRJAR 说它有一个新版本的用于 JDK16 的C2类,它覆盖了根目录中用于 JDK8 或更早版本的C2的版本。JDK16 版本还增加了一个名为C5的新类。
基于类似的理由,MRJAR 覆盖了类C1和C2,并为 JDK 版本 17 包含了一个名为C6的新类。
针对单个 MRJAR 中的多个 JDK 版本,MRJAR 中的搜索过程不同于常规的 JAR。在 MRJAR 中搜索资源或类文件使用以下规则:
-
JDK 的主要版本是根据使用 MRJAR 的环境来确定的。假设 JDK 的主要版本是
N。 -
为了定位名为
R的资源或类文件,从版本N的目录开始搜索META-INF/versions目录下的特定于平台的子目录。 -
如果在子目录
N中找到R,则返回。否则,搜索低于N版本的子目录。对于META-INF/versions目录下的所有子目录,该过程继续进行。 -
当在
META-INF/versions/N子目录中没有找到R时,在 MRJAR 的根目录中搜索R。
让我们以前面展示的 MRJAR 结构为例。假设程序正在寻找C3.class,JDK 的当前版本是 17。搜索将从META-INF/versions/17开始,在这里没有找到C3.class。搜索在META-INF/versions/16继续,在那里没有找到C3.class。现在搜索继续在根目录中进行,在那里找到了C3.class。
再举一个例子,假设你想在 JDK 版本是 17 的时候找到C2.class。搜索从META-INF/versions/17开始,在这里C2.class被找到并返回。
再举一个例子,假设你想在 JDK 版本是 16 的时候找到C2.class。搜索从META-INF/versions/16开始,在这里C2.class被找到并返回。
再举一个例子,假设你想在 JDK 版本是 8 的时候找到C2.class。没有名为META-INF/versions/8的 JDK8 专用目录。因此,搜索从根目录开始,在那里找到并返回C2.class。
注意所有处理震击器的工具——如java、javac和javap——都能够使用多释放震击器。处理 jar 的 API 也知道如何处理多版本 jar。
创建多版本 jar
一旦知道了在特定的 JDK 版本上搜索资源或类文件时 MRJAR 中目录的搜索顺序,就很容易理解如何找到类和资源了。JDK 版本特定目录的内容有一些规则。我将在后续章节中描述这些规则。在这一节中,我将重点介绍如何创建 MRJARs。
要运行此示例,您需要在计算机上安装 JDK8 和 JDK17。
我使用 MRJAR 来存储应用程序的 JDK8 和 JDK17 版本。该应用程序由以下两个类组成:
-
com.jdojo.mrjar.Main -
com.jdojo.mrjar.TimeUtil
Main类创建了一个TimeUtil类的对象,并在其中调用了一个方法。Main类可以作为main类来运行应用程序。TimeUtil类包含一个getLocalDate(Instant now)方法,该方法将一个Instant作为参数,并返回一个LocalDate来解释当前时区的时间。JDK17 在LocalDate类中有一个方法,命名为ofInstant(Instant instant, ZoneId zone)。我们将更新应用程序以使用 JDK17 来利用这种方法,并将保留出于相同目的使用 JDK8 时间 API 的旧应用程序。
本书的源代码包含两个项目。jdk17book目录下的主项目包含一个用于 JDK17 的名为jdojo.mrjar的模块。jdk17book\jdojo.mrjar.jdk8目录包含一个名为jdojo.mrjar.jdk8的项目,该项目包含 JDK8 代码。
清单 12-1 和 12-2 分别包含 JDK8 的TimeUtil和Main类的代码。这些项目的源代码很简单,所以我不会提供任何解释。我本可以将TimeUtil类中的getLocalDate()方法变成静态方法。我将它作为一个实例方法保存,所以您可以在输出(稍后讨论)中看到类的哪个版本被实例化了。当您运行Main类时,它打印当前的本地日期,当您运行这个例子时可能会有所不同。
// Main.java
package com.jdojo.mrjar;
import java.time.Instant;
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
System.out.println(
"Inside JDK 8 version of Main.main()...");
TimeUtil t = new TimeUtil();
LocalDate ld = t.getLocalDate(Instant.now());
System.out.println("Local Date: " + ld);
}
}
Inside JDK 8 version of Main.main()...
Creating JDK 8 version of TimeUtil...
Local Date: 2021-09-22
Listing 12-2A Main Class for JDK8
// TimeUtil.java
package com.jdojo.mrjar;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
public class TimeUtil {
public TimeUtil() {
System.out.println(
"Creating JDK 8 version of TimeUtil...");
}
public LocalDate getLocalDate(Instant now) {
return now.atZone(ZoneId.systemDefault())
.toLocalDate();
}
}
Listing 12-1A TimeUtil Class for JDK8
我们将把所有的 JDK17 类放在一个名为jdojo.mrjar的模块中,其声明如清单 12-3 所示。清单 12-4 和 12-5 分别包含 JDK17 的TimeUtil和Main类的代码。
// Main.java
package com.jdojo.mrjar;
import java.time.Instant;
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
System.out.println(
"Inside JDK 17 version of Main.main()...");
TimeUtil t = new TimeUtil();
LocalDate ld = t.getLocalDate(Instant.now());
System.out.println("Local Date: " + ld);
}
}
Inside JDK 17 version of Main.main()...
Creating JDK 17 version of TimeUtil...
Local Date: 2021-09-22
Listing 12-5A Main Class for JDK17
// TimeUtil.java
package com.jdojo.mrjar;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
public class TimeUtil {
public TimeUtil() {
System.out.println(
"Creating JDK 17 version of TimeUtil...");
}
public LocalDate getLocalDate(Instant now) {
return LocalDate.ofInstant(now,
ZoneId.systemDefault());
}
Listing 12-4A TimeUtil Class for JDK17
// module-info.java
module jdojo.mrjar {
exports com.jdojo.mrjar;
}
Listing 12-3A Module Declaration for a Module Named com.jdojo.mrjar
我已经展示了在 JDK8 和 JDK17 上运行Main类时的输出。然而,这个例子的目的不是单独运行这两个类,而是将它们打包在一个 MRJAR 中,并从这个 MRJAR 中运行它们,稍后我将向您展示这一点。
为了处理 MRJARs,jar工具接受一个名为– release的选项。其语法如下:
jar <options> --release N <other-options>
在这里,N是一个 JDK 的主要版本,如 17 为 JDK17。N的值必须大于或等于 9。跟随–release N选项的所有文件都被添加到 MRJAR 的META-INF/versions/N目录中。
下面的命令创建一个名为jdojo.mrjar.jar的 MRJAR,并将其放在C:\jdk17book\mrjars目录中。在运行以下命令之前,确保输出目录mrjars已经存在:
C:\jdk17book>jar --create --file mrjars\jdojo.mrjar.jar ^
-C jdojo.mrjar.jdk8\build\classes . ^
--release 17 -C build\modules\jdojo.mrjar .
注意这个命令中–release 17选项的使用。来自build\modules\jdojo.mrjar目录的所有文件都将被添加到 MRJAR 中的META-INF/versions/17目录。来自jdojo.mrjar.jdk8\build\classes目录的所有文件都将被添加到 MRJAR 的根目录中。MRJAR 中的条目将如下所示:
- jar-root
- com
- jdojo
- mrjar
- Main.class
- TimeUtil.class
- META-INF
- MANIFEST.MF
- versions
- 17
- module-info.class
- com
- jdojo
- mrjar
- Main.class
- TimeUtil.class
在创建 MRJARs 时,将–verbose选项与jar工具一起使用非常有帮助。该选件打印出许多有助于诊断错误的有用信息。下面是和以前一样的命令,但是带有–verbose选项。输出显示复制了哪些文件及其位置:
C:\jdk17book>jar --create --verbose ^
--file mrjars\jdojo.mrjar.jar ^
-C jdojo.mrjar.jdk8\build\classes . ^
--release 17 -C build\modules\jdojo.mrjar .
added manifest
added module-info: META-INF/versions/17/module-info.class
adding: com/(in = 0) (out= 0)(stored 0%)
adding: com/jdojo/(in = 0) (out= 0)(stored 0%)
adding: com/jdojo/mrjar/(in = 0) (out= 0)(stored 0%)
adding: com/jdojo/mrjar/Main.class(in = 1098)
(out= 591)(deflated 46%)
adding: com/jdojo/mrjar/TimeUtil.class(in = 884)
(out= 503)(deflated 43%)
adding: META-INF/versions/17/(in = 0)
(out= 0)(stored 0%)
adding: META-INF/versions/17/com/(in = 0)
(out= 0)(stored 0%)
adding: META-INF/versions/17/com/jdojo/(in = 0)
(out= 0)(stored 0%)
adding: META-INF/versions/17/com/jdojo/mrjar/(in = 0)
(out= 0)(stored 0%)
adding: META-INF/versions/17/com/jdojo/mrjar/Main.class
(in = 1326) (out= 688)(deflated 48%)
adding: META-INF/versions/17/com/jdojo/mrjar/TimeUtil.class
(in = 814) (out= 470)(deflated 42%)
假设您想要为 JDK 版本 8、16 和 17 创建一个 MRJAR。假设jdojo.mrjar.jdk16\modules\jdojo.mrjar目录包含特定于 JDK16 的类,下面的命令将完成这项工作:
C:\jdk17book>jar --create --verbose ^
--file mrjars\jdojo.mrjar.jar ^
-C jdojo.mrjar.jdk8\build\classes . ^
--release 17 -C build\modules\jdojo.mrjar . ^
--release 16 -C jdojo.mrjar.jdk16\modules\jdojo.mrjar .
您可以使用–list选项来验证 MRJAR 中的条目,如下所示:
C:\jdk17book>jar -list --file mrjars\jdojo.mrjar.jar
META-INF/
META-INF/MANIFEST.MF
META-INF/versions/17/module-info.class
com/
com/jdojo/
com/jdojo/mrjar/
com/jdojo/mrjar/Main.class
com/jdojo/mrjar/TimeUtil.class
META-INF/versions/17/
META-INF/versions/17/com/
META-INF/versions/17/com/jdojo/
META-INF/versions/17/com/jdojo/mrjar/
META-INF/versions/17/com/jdojo/mrjar/Main.class
META-INF/versions/17/com/jdojo/mrjar/TimeUtil.class
假设您有一个包含 JDK8 的资源和类文件的 JAR,您想通过添加 JDK17 的资源和类文件来更新这个 JAR,使它成为一个 MRJAR。您可以通过使用–update选项更新 JAR 的内容来做到这一点。以下命令创建一个仅包含 JDK8 文件的 JAR:
C:\jdk17book>jar --create --file mrjars\jdojo.mrjar.jar ^
-C jdojo.mrjar.jdk8\build\classes .
以下命令更新 JAR,使其成为 MRJAR:
C:\jdk17book>jar --update ^
--file mrjars\com.jdojo.mrjar.jar ^
--release 17 -C com.jdojo.mrjar.jdk17\build\classes .
C:\jdk17book>jar --update ^
--file mrjars\jdojo.mrjar.jar ^
--release 17 -C build\modules\jdojo.mrjar .
看看这个 MRJAR 的运行情况。下面的命令运行com.jdojo.mrjar包中的Main类,将 MRJAR 放在类路径上。JDK8 用于运行该类:
C:\jdk17book>C:\java8\bin\java ^
-classpath mrjars\jdojo.mrjar.jar ^
com.jdojo.mrjar.Main
Inside JDK 8 version of Main.main()...
Creating JDK 8 version of TimeUtil...
Local Date: 2021-09-22
输出显示,Main和TimeUtil这两个类都是从 MRJAR 的根目录中使用的,因为 JDK8 不支持 MRJAR。下面的命令使用模块路径运行相同的类。JDK17 用于运行命令:
C:\jdk17book>C:\java17\bin\java ^
--module-path mrjars\jdojo.mrjar.jar ^
--module jdojo.mrjar/com.jdojo.mrjar.Main
Inside JDK 17 version of Main.main()...
Creating JDK 17 version of TimeUtil...
Local Date: 2021-09-22
输出显示,Main和TimeUtil这两个类都是从 MRJAR 的META-INF/versions/17目录中使用的,因为 JDK17 支持 MRJAR,并且 MRJAR 拥有这些类的特定于 JDK17 的版本。
让我们稍微改变一下这个 MRJAR。创建一个具有相同内容的 MRJAR,但是在META-INF/versions/17目录中没有Main.class文件。在真实的场景中,在应用程序的 JDK17 版本中,只有TimeUtil类发生了变化,因此不需要为 JDK17 打包Main类。JDK8 的Main类也可以用在 JDK17 上。以下命令打包了我们上次所做的一切,除了 JDK17 的Main类。产生的 MRJAR 被命名为jdojo.mrjar2.jar。
C:\jdk17book>jar --create ^
--file mrjars\jdojo.mrjar2.jar ^
-C jdojo.mrjar.jdk8\build\classes . ^
--release 17 ^
-C build\modules\jdojo.mrjar ^
module-info.class ^
-C build\modules\jdojo.mrjar ^
com\jdojo\mrjar\TimeUtil.class
您可以使用以下命令验证新 MRJAR 的内容:
C:\jdk17book>jar --list --file mrjars\jdojo.mrjar2.jar
META-INF/
META-INF/MANIFEST.MF
META-INF/versions/17/module-info.class
com/
com/jdojo/
com/jdojo/mrjar/
com/jdojo/mrjar/Main.class
com/jdojo/mrjar/TimeUtil.class
META-INF/versions/17/com/jdojo/mrjar/TimeUtil.class
如果在 JDK8 上运行Main类,将会得到和以前一样的输出。但是,在 JDK17 上运行它会得到不同的输出:
C:\jdk17book>C:\java17\bin\java ^
--module-path mrjars\jdojo.mrjar2.jar ^
--module jdojo.mrjar/com.jdojo.mrjar.Main
Inside JDK 8 version of Main.main()...
Creating JDK 17 version of TimeUtil...
Local Date: 2021-09-22
输出显示从 JAR 根目录使用了Main类,而从META-INF/versions/17目录使用了TimeUtil类。请注意,您将获得不同的本地日期值。它在你的机器上打印当前日期。
多版本 jar 的规则
在创建多版本 jar 时,您需要遵循一些规则。如果你犯了一个错误,jar工具将打印错误。有时,错误消息并不直观。正如我所建议的,最好运行带有–verbose选项的jar工具,以获得关于错误的更多细节。
大多数规则基于一个事实:一个 MRJAR 包含一个用于多个 JDK 平台的库(或应用程序)的一个版本的 API。例如,您有一个名为jdojo-lib-1.0.jar的 MRJAR,它可能包含名为jdojo-lib的库的 API 的 1.0 版本,并且该库可能使用来自 JDK8 和 JDK17 的 API。这意味着当这个 MRJAR 用于类路径上的 JDK8、类路径上的 JDK17 或模块路径上的 JDK17 时,它应该提供相同的 API(就公共类型及其公共成员而言)。如果 MRJAR 在 JDK8 和 JDK17 上提供不同的 API,这就不是有效的 MRJAR。以下部分描述了一些规则。
MRJAR 可以是模块化的 JAR,在这种情况下,它可以在根目录、一个或多个版本化的目录或者两者的组合中包含一个模块描述符module-info.class。版本化描述符必须与根模块描述符相同,但有一些例外:
-
一个版本化描述符可以有不同的不可传递的
java.*和jdk.*模块的requires语句。 -
对于非 JDK 模块,不同的模块描述符不能有不同的非传递性
requires语句。 -
版本化描述符可以有不同的
uses语句。
这些规则基于这样一个事实,即实现细节的改变是允许的,但 API 本身不允许。允许对非 JDK 模块的requires语句进行更改被认为是 API 中的一项更改——它要求您为不同版本的 JDK 拥有不同的用户定义模块。这就是不允许这样做的原因。
模块化 MRJAR 不需要在根目录中有模块描述符。这就是我们在前一节的例子中所拥有的。我们在根目录中没有模块描述符,但是在META-INF/versions/17目录中有一个。这种安排使得在一个 MRJAR 中包含 JDK8 的非模块化代码和 JDK17 的模块化代码成为可能。
如果在版本化目录中添加一个新的公共类型,而该目录不在根目录中,那么在创建 MRJAR 时会收到一个错误。假设,在我们的例子中,您为 JDK17 添加了一个名为Test的公共类。如果Test类在com.jdojo.mrjar包中,它将被模块导出,并可用于 MRJAR 之外的代码。注意,根目录不包含Test类,所以这个 MRJAR 为 JDK8 和 JDK17 提供了不同的公共 API。在这种情况下,在 JDK17 的com.jdojo.mrjar包中添加一个公共的Test类会在创建 MRJAR 时产生一个错误。
继续同一个例子,假设您将Test类添加到 JDK17 的com.jdojo.test包中。请注意,该模块不导出此包。当您在模块路径上使用这个 MRJAR 时,外部代码将无法访问Test类。在这个意义上,这个 MRJAR 为 JDK8 和 JDK17 提供了相同的公共 API。然而,有一个条件!您还可以将这个 MRJAR 放在 JDK17 中的类路径上,在这种情况下,外部代码可以访问Test类——这违反了模块化封装,也违反了 MRJAR 应该跨 JDK 版本提供相同公共 API 的规则。因此,向 MRJAR 中模块的非导出包添加公共类型也是不允许的。如果您尝试这样做,将会收到类似以下内容的错误消息:
entry: META-INF/versions/17/com/jdojo/test/Test.class,
contains a new public class not found
in base entries
invalid multi-release jar file mrjars\jdojo.mrjar.jar
deleted
有时,有必要为同一个库添加更多类型来支持 JDK 的新版本。必须添加这些类型来支持更新的实现。您可以通过向 MRJAR 中的版本化目录添加包私有类型来实现这一点。在本例中,如果将类设为非公共的,可以为 JDK17 添加Test类。
引导加载程序不支持多版本 jar,例如,使用-Xbootclasspath/a选项指定 MRJARs。对于一个很少需要的特性,支持这一点会使引导加载程序的实现变得复杂。
MRJAR 应该在一个版本化的目录中包含同一个文件的不同版本。如果一个资源或类文件在不同的平台版本中是相同的,那么这样的文件应该被添加到根目录中。目前,jar工具会发出警告,如果它在多个版本目录中看到相同的条目,并且内容相同。
多版本 JAR 和 JAR URL
在 MRJARs 之前,JAR 中的所有资源都位于根目录下。当您从类加载器(ClassLoader.getResource( "com/jdojo/mrjar/TimeUtil.class" ))请求资源时,返回的 URL 如下所示:
jar:file:/C:/jdk17book/mrjars/jdojo.mrjar.jar!
com/jdojo/mrjar/TimeUtil.class
使用 MRJARs,可以从根目录或版本化目录返回资源。如果您正在 JDK17 上查找TimeUtil.class文件,URL 如下:
jar:file:/C:/jdk17book/mrjars/jdojo.mrjar.jar!
/META-INF/versions/17/com/jdojo/mrjar/TimeUtil.class
如果您现有的代码需要特定格式的资源的jar URL,或者您手工编码了一个jar URL,那么您可能会得到令人惊讶的结果。如果您用 MRJARs 重新打包您的 JARs,您需要再次查看您的代码,并修改它以使用 MRJARs。
多版本清单属性
MRJAR 在其MANIFEST.MF文件中包含一个特殊的属性条目:
Multi-Release: true
Multi-Release属性是由 MRJAR 的jar工具添加的。如果这个属性的值是true,这意味着这个 JAR 是一个多版本 JAR。如果它的值是false或者属性缺失,那么它就不是一个多释放的 JAR。该属性被添加到清单文件的主节中。
名为MULTI_RELEASE的常量被添加到Attributes.Name类中,该类位于java.util.jar包中,用来表示清单文件中的属性Multi-Release。因此,Attributes.Name.MULTI_RELEASE常量代表 Java 代码中Multi-Release属性的值。
JMOD 格式
Java 提供了另一种叫做 JMOD 的格式来打包模块。JMOD 文件比 JAR 文件能处理更多的内容类型。JMOD 文件可以打包本机代码、配置文件、本机命令和其他类型的数据。JMOD 格式基于 ZIP 格式,所以您可以使用标准的 ZIP 工具来查看它们的内容。JDK 模块以 JMOD 格式打包,供您在编译时和链接时使用。运行时不支持 JMOD 格式。您可以在JDK_HOME\jmods目录中找到 JMOD 格式的 JDK 模块,其中JDK_HOME是您安装 JDK 的目录。您可以将自己的模块打包成 JMOD 格式。JMOD 格式的文件有一个.jmod扩展名。例如,名为java.base的平台模块已经打包在java.base.jmod文件中。
JMOD 文件可以包含本地代码,在运行时动态提取和链接这些代码有点棘手。这就是 JMOD 文件在编译时和链接时受支持,但在运行时不受支持的原因。
使用 jmod 工具
虽然您可以使用 ZIP 工具来处理 JMOD 文件,但是 JDK 附带了一个特别定制的工具,名为jmod。它位于JDK_HOME\bin目录中。它可以用来创建 JMOD 文件,列出 JMOD 文件的内容,打印模块的描述,以及记录所使用的模块的散列。使用jmod工具的一般语法如下:
jmod <subcommand> <options> <jmod-file>
必须将下列子命令之一与jmod命令一起使用:
-
create -
extract -
list -
describe -
hash
list和describe子命令不接受任何选项。<jmod-file>是您正在创建的 JMOD 文件或者您想要描述的现有 JMOD 文件。表 12-1 包含工具支持的选项列表。
表 12-1
jmod 工具的选项列表
|[计]选项
|
描述
|
| --- | --- |
| –class-path <path> | 指定可以找到要打包的类的类路径。<path>可以是包含应用程序类的 JAR 文件或目录的路径列表。<path>处的内容将被复制到 JMOD 文件中。 |
| –cmds <path> | 指定包含本机命令的目录列表,这些命令需要复制到 JMOD 文件中。 |
| –config <path> | 指定包含要复制到 JMOD 文件的用户可编辑配置文件的目录列表。 |
| –dir <path> | 指定将提取指定 JMOD 文件内容的目标目录。 |
| –do-not-resolve-by-default | 如果使用此选项创建 JMOD 文件,JMOD 文件中包含的模块将从默认的根模块集中排除。要解析这样一个模块,您必须使用–add-modules命令行选项将它添加到默认的根模块集中。 |
| –dry-run | 模拟运行模块散列。使用此选项可以计算和打印散列,但不会将它们记录在 JMOD 文件中。 |
| –exclude <pattern-list> | 排除与提供的逗号分隔模式列表匹配的文件,每个元素使用以下形式之一:<glob-pattern>、glob:<glob-pattern>或regex:<regex-pattern>。 |
| –hash-modules <regex-pattern> | 计算并记录散列,将打包的模块与匹配给定的<regex-pattern>并直接或间接依赖于它的模块联系起来。散列记录在正在创建的 JMOD 文件中,或者用jmod hash命令指定的模块路径上的 JMOD 文件或模块化 JAR 中。 |
| –help, -h | 打印jmod命令的用法说明和所有选项列表。 |
| –header-files <path> | 将路径列表指定为<path>,本地代码的头文件将被复制到 JMOD 文件中。 |
| –help-extra | 打印关于jmod工具支持的附加选项的帮助。 |
| –legal-notices <path> | 指定要复制到 JMOD 文件的法律声明的位置。 |
| –libs <path> | 指定包含要复制到 JMOD 文件的本地库的目录列表。 |
| –main-class <class-name> | 指定用于运行应用程序的主类名。 |
| –man-pages <path> | 指定手册页的位置。 |
| –module-version <version> | 指定要记录在module-info.class文件中的模块版本。 |
| –module-path <path>, -p<path> | 指定用于查找哈希模块的模块路径。 |
| –target-platform<platform> | <platform>以<os>-<arch>的形式指定,例如windows-amd64和linux-amd64。该选项指定目标操作系统和架构,记录在module-info.class文件的ModuleTarget属性中。 |
| –version | 打印jmod工具的版本。 |
| –warn-if-resolved <reason> | 指定一个提示给jmod工具,如果一个模块被解析,它将发出一个警告,这个模块已经被弃用,被弃用删除,或者正在酝酿。<reason>的值可以是以下三个值之一:deprecated、deprecated-for-removal或incubating。 |
| @<filename> | 从指定文件中读取选项。 |
以下章节详细解释了如何使用jmod命令。本章中使用的所有命令都应输入到一行中。有时,为了清晰起见,我在书中用多行展示它们。
您可以使用带有jmod工具的create子命令创建一个 JMOD 文件。JMOD 文件的内容是模块的内容。假设存在以下目录和文件:
C:\jdk17book\jmods
C:\jdk17book\dist\jdojo.javafx.jar
下面的命令在C:\jdk17book\jmods目录中创建一个jdojo.javafx.jmod文件。JMOD 文件的内容来自jdojo.javafx.jar文件:
C:\jdk17book>jmod create ^
--class-path dist\jdojo.javafx.jar ^
jmods\jdojo.javafx.jmod
通常,JMOD 文件的内容来自一组包含模块编译代码的目录。以下命令创建一个jdojo.javafx.jmod文件。它的内容来自一个build\modules\jdojo.javafx目录。该命令使用–module-version选项来设置模块版本,该版本将被记录在build\modules\jdojo.javafx目录下的module-info.class文件中。在运行以下命令之前,请确保删除在上一步中创建的 JMOD 文件:
C:\jdk17book>jmod create --module-version 1.0 ^
--class-path build\modules\jdojo.javafx ^
jmods\jdojo.javafx.jmod
您可以用这个 JMOD 文件做什么?您可以将它放在模块路径上,以便在编译时使用。您可以使用它和jlink工具来创建一个定制的运行时映像,您可以用它来运行您的应用程序。回想一下,您不能在运行时使用 JMOD 文件。如果您试图通过将 JMOD 文件放在模块路径上来在运行时使用它,您将收到以下错误:
Error occurred during initialization of VM
java.lang.module.ResolutionException:
JMOD files not supported: jmods\jdojo.javafx.jmod
...
Extracting JMOD File Contents
您可以使用extract子命令提取 JMOD 文件的内容。以下命令将jmods\jdojo.javafx.jmod文件的内容提取到名为extracted的目录中:
C:\jdk17book>jmod extract --dir extracted ^
jmods\jdojo.javafx.jmod
如果没有–dir选项,JMOD 文件的内容将被提取到当前目录中。
您可以使用带有jmod工具的list子命令来打印 JMOD 文件中所有条目的名称。以下命令列出了您在上一节中创建的jdojo.javafx.jmod文件的内容:
C:\jdk17book>jmod list jmods\jdojo.javafx.jmod
classes/module-info.class
classes/com/jdojo/javafx/BindingTest.class
...
classes/resources/fxml/sayhello.fxml
下面的命令列出了java.base模块的内容,该模块作为一个名为java.base.jmod的 JMOD 文件提供。该命令假设您已经在C:\java17目录中安装了 JDK。输出超过 120 页。显示了部分输出。注意,JMOD 文件在内部将不同类型的内容存储在不同的目录中。
C:\jdk17book>jmod list C:\java17\jmods\java.base.jmod
classes/module-info.class
classes/java/nio/file/WatchEvent.class
classes/java/nio/file/WatchKey.class
bin/java.exe
bin/javaw.exe
native/amd64/jvm.cfg
native/java.dll
conf/net.properties
conf/security/java.policy
conf/security/java.security
...
您可以使用带有jmod工具的describe子命令来描述 JMOD 文件中包含的模块。以下命令描述了包含在jdojo.javafx.jmod文件中的模块:
C:\jdk17book>jmod describe jmods\jdojo.javafx.jmod
jdojo.javafx@1.0
exports com.jdojo.javafx
requires java.base mandated
requires javafx.controls
requires javafx.fxml
contains resources.fxml
您可以使用这个命令描述平台模块。以下命令描述了包含在java.sql.jmod中的模块,假设您在C:\java17目录中安装了 JDK:
C:\jdk17book>jmod describe C:\java17\jmods\java.sql.jmod
java.sql@9.0.1
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires java.base mandated
requires java.logging transitive
requires java.xml transitive
uses java.sql.Driver
platform windows-amd64
您可以使用带有jmod工具的hash子命令来记录 JMOD 文件中包含的模块的module-info.class文件中其他模块的散列。这些散列将在以后用于依赖性验证。假设您在四个 JMOD 文件中有四个模块:
-
jdojo.prime -
jdojo.prime.faster -
jdojo.prime.probable -
jdojo.prime.client
假设您想将这些模块交付给您的客户机,并确保模块代码保持不变。您可以通过在jdojo.prime模块中记录jdojo.prime.faster、jdojo.prime.probable和jdojo.prime.client模块的散列来实现这一点。让我们看看如何实现这一点。
为了计算其他模块的哈希值,jmod工具需要找到这些模块。您需要使用–module-path选项来指定其他模块所在的模块路径。您还需要使用–hash-modules选项来指定需要记录其散列的模块所使用的模式列表。
注意当你把一个模块打包成一个模块 JAR 时,你也可以使用–hash-modules和–module-path选项和jar工具来记录依赖模块的散列。
使用以下四个命令为四个模块创建 JMOD 文件。注意,我在创建com.jdojo.prime.client.jmod文件时使用了–main-class选项。我在第十三章中讨论jlink工具时再次用到它。如果在运行这些命令时出现“文件已经存在”错误,请从jmods目录中删除现有的 JMOD 文件,然后重新运行该命令:
C:\jdk17book>jmod create --module-version 1.0 ^
--class-path build\modules\jdojo.prime ^
jmods\jdojo.prime.jmod
C:\jdk17book>jmod create --module-version 1.0 ^
--class-path build\modules\jdojo.prime.faster ^
jmods\jdojo.prime.faster.jmod
C:\jdk17book>jmod create --module-version 1.0 ^
--class-path build\modules\jdojo.prime.probable ^
jmods\jdojo.prime.probable.jmod
C:\jdk17book>jmod create --module-version 1.0 ^
--class-path build\modules\jdojo.prime.client ^
jmods\jdojo.prime.client.jmod
现在,您已经准备好使用下面的命令来记录名称以jdojo.prime开头的所有模块的散列了:
C:\jdk17book>jmod hash ^
--module-path jmods ^
--hash-modules jdojo.prime.? jmods\jdojo.prime.jmod
Hashes are recorded in module jdojo.prime
让我们看看记录在com.jdojo.prime模块中的散列。以下命令打印模块描述以及记录在com.jdojo.prime模块中的散列:
C:\jdk17book>jmod describe jmods\jdojo.prime.jmod
jdojo.prime@1.0
exports com.jdojo.prime
requires java.base mandated
uses com.jdojo.prime.PrimeChecker
provides com.jdojo.prime.PrimeChecker with
com.jdojo.prime.impl.genericprimechecker
contains com.jdojo.prime.impl
hashes jdojo.prime.client SHA-256
5950...6ce95e9849f520f4b9f54bc520d7969c396dc4f93805121b
hashes jdojo.prime.faster SHA-256
5538...4e264cfa12848be32d3f0b9a5df506aa57ba4443dfcbdc6a
hashes jdojo.prime.probable SHA-256
a1b8...5d62313de97ee285ed845895c8ef3c52b53a16370dd3b2d5
当您使用create子命令创建一个新的 JMOD 文件时,您也可以记录其他模块的散列。假设三个模块jdojo.prime.faster、jdojo.prime.probable和jdojo.prime.client存在于模块路径上,您可以使用下面的命令来创建jdojo.prime.jmod文件,该文件也将记录这三个模块的散列:
C:\jdk17book>jmod create --module-version 1.0 ^
--module-path jmods ^
--hash-modules jdojo.prime.? ^
--class-path build\modules\jdojo.prime ^
jmods\jdojo.prime.jmod
您可以为 JMOD 文件模拟运行散列过程,其中散列将被打印,但不会被记录。在不创建 JMOD 文件的情况下,模拟运行选项对于确保所有设置都是正确的非常有用。以下命令序列将引导您完成该过程。首先,删除您在上一步中创建的jmods\jdojo.prime.jmod文件。
以下命令创建了jmods\jdojo.prime.jmod文件,但没有记录任何其他模块的哈希:
C:\jdk17book>jmod create --module-version 1.0 ^
--module-path jmods ^
--class-path build\modules\jdojo.prime ^
jmods\jdojo.prime.jmod
以下命令模拟运行hash子命令。它计算并打印其他模块的散列,匹配在–hash-modules选项中指定的正则表达式。没有散列将被记录在jmods\jdojo.javafx.jmod文件中:
C:\jdk17book>jmod hash --dry-run ^
--module-path jmods ^
--hash-modules jdojo.prime.? ^
jmods\jdojo.prime.jmod
Dry run:
jdojo.prime
hashes jdojo.prime.client SHA-256
5950...6ce95e9849f520f4b9f54bc520d7969c396dc4f93805121b
hashes jdojo.prime.faster SHA-256
5538...4e264cfa12848be32d3f0b9a5df506aa57ba4443dfcbdc6a
hashes jdojo.prime.probable SHA-256
a1b8...5d62313de97ee285ed845895c8ef3c52b53a16370dd3b2d5
以下命令验证前面的命令没有在 JMOD 文件中记录任何哈希:
C:\jdk17book>jmod describe jmods\jdojo.prime.jmod
jdojo.prime@1.0
exports com.jdojo.prime
requires java.base mandated
uses com.jdojo.prime.PrimeChecker
provides com.jdojo.prime.PrimeChecker with
com.jdojo.prime.impl.genericprimechecker
contains com.jdojo.prime.impl
当您使用jlink工具创建自定义运行时映像时,您将在第十三章中再次看到 JMOD 文件的运行。
摘要
Java 支持四种格式来打包模块:展开的目录、JAR 文件、JMOD 文件和 JIMAGE 文件。JAR 格式在 JDK9 中得到增强,以支持模块化 JAR 和多版本 JAR。多版本 JAR 允许您针对不同版本的 JDK 打包相同版本的库或应用程序。例如,一个多版本 JAR 可能包含库版本 1.2 的代码,该版本包含 JDK8 和 JDK17 的代码。当在 JDK8 上使用多版本 JAR 时,将使用库代码的 JDK8 版本。在 JDK17 上使用时,将使用 JDK17 版本的库代码。特定于 JDK 版本N的文件存储在多版本 JAR 的META-INF\versions\N目录中。所有 JDK 版本通用的文件存储在根目录中。对于不支持多版本 jar 的环境,此类 jar 被视为常规 jar。在多版本 JAR 中,文件的搜索顺序是不同的——在搜索根目录之前,先搜索以当前平台的主要版本开始的所有版本化目录。
JMOD 文件比 JAR 文件能处理更多的内容类型。它们可以打包本机代码、配置文件、本机命令和其他类型的数据。JDK 模块以 JMOD 格式打包,供您在编译时和链接时使用。运行时不支持 JMOD 格式。您可以使用jmod工具来处理 JMOD 文件。
练习
练习 1
你可以用什么格式来打包你的模块?
练习 2
什么是多释放罐?
运动 3
描述多重释放震击器的结构。
演习 4
当在不理解多发布 JAR 的 JDK 版本(例如 JDK8)上使用多发布 JAR 时会发生什么?
锻炼 5
描述在多版本 JAR 中查找资源时的搜索顺序。
锻炼 6
描述多重释放震击器的局限性。
锻炼 7
多版本 JAR 的META-INF\MANIFEST.MF文件中的属性名是什么?
运动 8
什么是jmod工具,它位于哪里?
演习 9
什么是 JMOD 格式,它比 JAR 格式好在哪里?
运动 10
Java 支持三个阶段:编译时、链接时和运行时。在哪些阶段支持 JMOD 格式?
演习 11
假设您有一个名为jdojo.test.jmod的 JMOD 文件。使用jmod工具编写命令来描述这个 JMOD 文件中存储的模块。
运动 12
JMOD 格式的 JDK 模块在哪里?