Java 9 揭秘(6. 封装模块)

Tips
做一个终身学习的人。

Java 9

在这章节中, 主要介绍以下内容:

  • 封装Java模块的不同格式
  • JAR格式增强
  • 什么是多版本JAR
  • 如何创建和使用多版本JAR
  • JMOD是什么格式
  • 如何使用jmod工具来处理JMOD文件
  • 如何创建、解压和描述JMOD文件
  • 如何列出JMOD文件的内容
  • 如何在JMOD文件中记录模块的哈希值以进行依赖性验证

模块可以以不同的格式打包,以便在编译时,链接时和运行时三个阶段中使用。 但并不是在所有阶段都支持这四种格式。 JDK 9支持以下格式打包模块:

  • 展开的目录
  • JAR格式
  • JMOD格式
  • JIMAGE格式

在JDK 9之前支持展开的目录和JAR格式。JDK 9中的JAR格式已得到增强,以支持模块化JAR和多版本JAR。 JDK 9为封装模块引入了两种新格式:JMOD格式和JIMAGE格式。 本节主要讨论JAR格式和JMOD格式的增强。

一. JAR格式

在前面的第三者介绍了如何在jar工具中使用新的选项来创建模块化的JAR。 jar工具还用于列出JAR文件中的条目,并提取和更新JAR文件的内容。 该jar工具在JDK 9之前已经支持这些操作,并且在JDK 9中没有任何新的操作。在本章中,将介绍添加到JAR格式的新功能,称为多版本JAR。

1. 什么是多版本JAR

作为一名经验丰富的Java开发人员,你必须使用Java类库/框架,例如Spring框架,Hibernate等。您可能正在使用Java 8,但这些类库可能仍然在Java 6或Java 7中使用。为什么类库开发人员不能使用最新版的JDK来使用新功能? 其中一个原因是不是所有的类库使用者都使用最新的JDK。 更新类库以使用较新版本的JDK意味着强制所有类库用户迁移到较新的JDK,这在实践中是不可能的。 维护和发布针对不同JDK的类库是打包代码时的另一个痛苦。 通常,你将会找到一个用于不同JDK的单独的库JAR。 JDK 9通过为类库开发人员提供一种打包类库代码的新方法来解决这个问题,使用单个JAR包含多个JDK的类库的相同版本。 这样的JAR被称为多版本JAR。

多版本JAR(MRJAR)包含与多个JDK版本相同版本的类库(提供相同的API)。 也就是说,可以将类库作为可用于JDK 8和JDK 9的MRJAR。MRJAR中的代码将包含在JDK 8和JDK 9中编译的类文件。使用JDK 9编译的类可以利用JDK 9提供的API,而使用JDK 8编译的类可以提供使用JDK 8编写的相同的类库API。

MRJAR扩展了JAR的已有的目录结构。 JAR包含其所有内容所在的根目录。 它包含一个META-INF目录,用于存储有关JAR的元数据。 通常,JAR包含包含其属性的META-INF/MANIFEST.MF文件。 典型的JAR中的条目如下所示:

- jar-root
  - C1.class
  - C2.class
  - C3.class
  - C4.class
- META-INF
  - MANIFEST.MF

JAR包含四个class文件和一个MANIFEST.MF文件。 MRJAR扩展了META-INF目录以存储特定于JDK版本的类。 META-INF目录包含一个版本子目录,其中可能包含许多子目录,每个目录命名与JDK主要版本相同。 例如,对于特定于JDK 9的类,可能有META-INF/versions/9目录,对于JDK 10特定的类,可能有一个名为META-INF/versions/10的目录等。典型的MRJAR 可能有以下条目:

- jar-root
  - C1.class
  - C2.class
  - C3.class
  - C4.class
- META-INF
  - MANIFEST.MF
  - versions
    - 9
      - C2.class
      - C5.class
    - 10
      - C1.class
      - C2.class
      - C6.class

如果该MRJAR在不支持MRJAR的环境中使用,则将被视为常规JAR ——根目录中的内容将被使用,META-INF/version/9和 META-INF/versions/10目录下的类将被忽略。 因此,如果这个MRJAR与JDK 8一起使用,则只能使用四个类:C1,C2,C3和C4。

当在JDK 9中使用这个MRJAR时,有五个类可以执行:C1,C2,C3,C4和C5。 将使用META-INF/versions/9目录中的C2类,而不是根目录中的C2类。 在这种情况下,MRJAR表示它具有JDK 9的C2类的较新版本,该版本覆盖了JDK 8或更早版本的根目录中的C2版本。 JDK 9版本还添加了一个名为C5的新类。

同样,如果使用JDK 10,MRJAR会覆盖类C1和C2类,并且包含JDK版本10的名为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的根目录。

我们来看一下使用以前显示的MRJAR结构的例子。 假设程序正在寻找C3.class,当前版本的JDK是10,搜索将从META-INF/versions/10开始,其中找不到C3.class。 在META-INF/versions/9中继续搜索,其中找不到C3.class。 现在搜索继续在根目录中,最后找到C3.class。

另一个例子,假设你想在JDK版本为10时找到C2.class。搜索从META-INF/versions/10开始,其中找到并返回C2.class。

另一个例子,假设你想在JDK版本为9时找到C2.class。搜索从META-INF/versions/9开始,其中找到并返回C2.class。

另一个例子,假设你想在JDK版本为8时找到C2.class。没有名为META-INF/versions/8的JDK 8特定目录。 因此,搜索从根目录开始,找到并返回C2.class。

Tips
在JDK 9中,处理JAR的所有工具(如java,javac和javap)都被修改为使用多版本的JAR。 处理JAR的API也已经更新,以处理多版本的JAR。

2. 创建多版本JAR

当在特定的JDK版本中搜索资源或类文件后,我们已经知道MRJAR中目录的搜索顺序,很容易了解如何找到类和资源。 有关JDK版本特定目录内容的一些规则。 将在后面的章节中描述这些规则。 在本节中,将重点介绍创建MRJAR。

要运行此示例,需要在计算机上安装JDK 8和JDK 9。 如果没有JDK 8,除JDK 9之外的任何其他JDK都可以。 对于除版本8以外的JDK,将需要更改示例中的代码,因此代码将使用你本地版本的JDK进行编译。

使用MRJAR来存储应用程序的JDK 8和JDK 9版本。 该应用程序由以下两个类组成:

com.jdojo.mrjar.Main
com.jdojo.mrjar.TimeUtil

Main类创建一个TimeUtil类的对象,并调用它的一个方法。 Main类可以用作运行应用程序的主类。 TimeUtil类包含一个getLocalDate(Instant now)方法,它将Instant作为参数,并返回一个LocalDate类来表示当前时区的时间。 JDK 9已经为LocalDate类添加了一个新方法,它被命名为ofInstant(Instant instant, ZoneId zone)。 我们将更新应用程序以使用JDK 9并利用这种新方法,并保留使用JDK 8 中实现相同功能的Time API的旧应用程序。

源代码包含两个名为com.jdojo.mrjar.jdk8com.jdojo.mrjar.jdk9的NetBeans项目,它们分别配置为使用JDK 8和JDK 9。 在NetBeans中,需要将com.jdojo.mrjar.jdk8项目的源和库属性更改为JDK 8,并将 com.jdojo.mrjar.jdk9项目更改为JDK 9。这些项目的源代码很简单。 可以在TimeUtil类中创建一个静态方法的getLocalDate()方法。 在这里作为一个实例方法,所以你可以看到在输出(稍后讨论)哪个版本的实例化。 运行Main类时,会打印当前的本地日期,当你运行此示例时结果可能会有所不同。

下面包含使用JDK 8的TimeUtilMain类的代码。

// 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();
    }
}
// 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);
    }
}

下面包含使用JDK 9的TimeUtilMain类的代码模块声明为com.jdojo.mrjar。

// module-info.java
module com.jdojo.mrjar {
    exports com.jdojo.mrjar;
}
// 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 9 version of TimeUtil...");
    }
    public LocalDate getLocalDate(Instant now) {
        return LocalDate.ofInstant(now, ZoneId.systemDefault());
    }
}
// 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 9 version of Main.main()...");
        TimeUtil t = new TimeUtil();
        LocalDate ld = t.getLocalDate(Instant.now());
        System.out.println("Local Date: " + ld);
    }
}

这个例子的目的不是单独运行这两个类,而是将它们全部包装在MRJAR中并运行它们。

JDK 9中的jar工具已得到增强,以支持创建MRJAR。 在JDK 9中,jar工具接受一个新的选项,叫做--release。 其语法如下:

jar <options> --release N <other-options>

这里,N是一个JDK主版本,如JDK 9中的9。N的值必须大于或等于9。所有在--release N选项之后的所有文件将被添加到 MRJAR的META-INF/versions/N目录下。

以下命令创建名为com.jdojo.mrjar.jar的MRJAR,并将其放在C:\ Java9Revealed\mrjars目录下,该目录是已经存在的目录:

C:\Java9Revealed>jar --create --file mrjars\com.jdojo.mrjar.jar
-C com.jdojo.mrjar.jdk8\build\classes .
--release 9 -C com.jdojo.mrjar.jdk9\build\classes .

请注意在此命令中使用--release 9选项。 来自com.jdojo.mrjar.jdk9\ build\classes目录的所有文件将被添加到MRJAR中的META-INF/versions/9目录中。 来自com.jdojo.mrjar.jdk8\build\classes目录的所有文件将被添加到MRJAR的根目录下。 MRJAR中的条目将如下所示:

- jar-root
  - com
    - jdojo
      - mrjar
        - Main.class
        - TimeUtil.class
- META-INF
  - MANIFEST.MF
  - versions
    - 9
      - module-info.class
      - com
        - jdojo
          - mrjar
            - Main.class
            - TimeUtil.class

在创建MRJAR时,使用--verbose选项在jar工具中非常有帮助。 该选项打印出许多有用的信息,帮助诊断错误。 以下是与以前相同的命令,但使用了--verbose选项。 输出显示哪些文件被复制以及它们的位置:

C:\Java9Revealed>jar --create --verbose --file mrjars\com.jdojo.mrjar.jar
-C com.jdojo.mrjar.jdk8\build\classes .
--release 9 -C com.jdojo.mrjar.jdk9\build\classes .

输出信息为:

added manifest
added module-info: META-INF/versions/9/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 = 1100) (out= 592)(deflated 46%)
adding: com/jdojo/mrjar/TimeUtil.class(in = 884) (out= 503)(deflated 43%)
adding: META-INF/versions/9/(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/.netbeans_automatic_build(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/.netbeans_update_resources(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/com/(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/com/jdojo/(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/com/jdojo/mrjar/(in = 0) (out= 0)(stored 0%)
adding: META-INF/versions/9/com/jdojo/mrjar/Main.class(in = 1328) (out= 689)(deflated 48%)
adding: META-INF/versions/9/com/jdojo/mrjar/TimeUtil.class(in = 814) (out= 470)(deflated 42%)

假设要为MRJAR创建JDK的版本8,9和10。com.jdojo.mrjar.jdk10\build\classes目录包含特定于JDK 10的类,则以下命令将执行该任务:

C:\Java9Revealed>jar --create --file mrjars\com.jdojo.mrjar.jar
-C com.jdojo.mrjar.jdk8\build\classes .
--release 9 -C com.jdojo.mrjar.jdk9\build\classes .
--release 10 -C com.jdojo.mrjar.jdk10\build\classes .

可以使用--list选项验证MRJAR中的条目,如下所示:

C:\Java9Revealed>jar --list --file mrjars\com.jdojo.mrjar.jar

输出结果为:

META-INF/
META-INF/MANIFEST.MF
com/
com/jdojo/
com/jdojo/mrjar/
com/jdojo/mrjar/Main.class
com/jdojo/mrjar/TimeUtil.class
META-INF/versions/9/
META-INF/versions/9/com/
META-INF/versions/9/com/jdojo/
META-INF/versions/9/com/jdojo/mrjar/
META-INF/versions/9/com/jdojo/mrjar/Main.class
META-INF/versions/9/com/jdojo/mrjar/TimeUtil.class
META-INF/versions/9/module-info.class
META-INF/versions/10/
META-INF/versions/10/com/
META-INF/versions/10/com/jdojo/
META-INF/versions/10/com/jdojo/mrjar/
META-INF/versions/10/com/jdojo/mrjar/TimeUtil.class

假设有一个包含JDK 8的资源和类文件的JAR,并且希望通过为JDK 9添加资源和类文件来更新JAR以使其成为MRJAR。可以通过使用以下命令来更新JAR的内容: --update。 以下命令创建仅具有JDK 8文件的JAR:

C:\Java9Revealed>jar --create --file mrjars\com.jdojo.mrjar.jar
-C com.jdojo.mrjar.jdk8\build\classes .

以下命令更新JAR以使其成为MRJAR:

C:\Java9Revealed>jar --update --file mrjars\com.jdojo.mrjar.jar
--release 9 -C com.jdojo.mrjar.jdk9\build\classes .

看看这个MRJAR的运作。 以下命令运行com.jdojo.mrjar包中的Main类,将MRJAR放在类路径上。 JDK 8用于运行类。

C:\Java9Revealed> c:\java8\bin\java -classpath mrjars\com.jdojo.mrjar.jar com.jdojo.mrjar.Main

输出结果为:

Inside JDK 8 version of Main.main()...
Creating JDK 8 version of TimeUtil...
Local Date: 2017-06-27

输出显示,从MRJAR的根目录使用了两个类MainTimeUtil类,因为JDK 8不支持MRJAR。 以下命令使用模块路径运行相同的类。 在JDK 9中用于运行命令:

C:\Java9Revealed> c:\java9\bin\java --module-path mrjars\com.jdojo.mrjar.jar --module com.jdojo.mrjar/com.jdojo.mrjar.Main

输出结果为:

Inside JDK 9 version of Main.main()...
Creating JDK 9 version of TimeUtil...
Local Date: 2017-06-27

输出显示,从MRJAR的META-INF/versions/9目录中使用了两个类MainTimeUtil,因为JDK 9支持MRJAR,MRJAR具有JDK 9特有的这些类的版本。

让我们给这个MRJAR一点点变化。 创建具有相同内容的MRJAR,但在META-INF/versions/9目录中没有Main.class文件。 在现实世界的场景中,只有TimeUtil类在应用程序的JDK 9版本中发生变化,因此不需要为JDK 9打包Main类。JDK 8的Main类也可用于JDK 9。 以下命令打包我们上次执行的所有操作,除了JDK 9的Main类之外。生成的MRJAR命名为com.jdojo.mrjar2.jar。

C:\Java9Revealed>jar --create --verbose --file mrjars\com.jdojo.mrjar2.jar
-C com.jdojo.mrjar.jdk8\build\classes .
--release 9
-C com.jdojo.mrjar.jdk9\build\classes module-info.class
-C com.jdojo.mrjar.jdk9\build\classes com\jdojo\mrjar\TimeUtil.class

可以使用以下命令验证新MRJAR的内容:

C:\Java9Revealed>jar --list --file mrjars\com.jdojo.mrjar2.jar

输出的结果为:

META-INF/
META-INF/MANIFEST.MF
META-INF/versions/9/module-info.class
com/
com/jdojo/
com/jdojo/mrjar/
com/jdojo/mrjar/Main.class
com/jdojo/mrjar/TimeUtil.class
META-INF/versions/9/com/jdojo/mrjar/TimeUtil.class

如果在JDK 8上运行Main类,将获得与以前相同的输出。 但是,在JDK 9上运行它会给你一个不同的输出:
命令行命令:

C:\Java9Revealed>c:\java9\bin\java --module-path mrjars\com.jdojo.mrjar2.jar --module com.jdojo.mrjar/com.jdojo.mrjar.Main

输出结果为:

Inside JDK 8 version of Main.main()...
Creating JDK 9 version of TimeUtil...
Local Date: 2017-06-27

输出显示Main类是在JAR根目录使用的,而TimeUtil类使用的是META-INF/versions/9目录下的。

4. 多版本JAR的规则

创建多版本JAR时,需要遵循一些规则。 如果你犯了一个错误,jar工具会打印错误信息。 有时,错误信息不直观。最好使用--verbose选项运行jar工具来获取有关错误的更多详细信息。

大多数规则基于一个事实:MRJAR包含用于多个JDK平台的类库(或应用程序)的同一个版本的API。 例如,有一个名为jdojo-lib-1.0.jar的MRJAR,可能包含名为jdojo-lib的库的1.0版本,该库可能使用JDK 8和JDK 9中的API。这意味着该MRJAR应该提供一份相同的API(在公共类型及其公共成员方面),用在JDK 8的类路径上,或JDK 9的类路径或模块路径上。 如果MRJAR在JDK 8和JDK 9上提供不同的API,那么这不是有效的MRJAR。

二. 模块化多版本JAR

MRJAR可以是模块化JAR,在这种情况下,它可以在根目录中包含一个模块描述符module-info.class,在一个或多个版本化目录中,或两者的组合。 版本化描述符必须与根模块描述符相同,但有一些例外:

  • 一个版本描述符可以有不同的以java.* jdk.*开头模块的非传递性requires 语句。
  • 不同的模块描述符对于非JDK模块不能有不同的非传递性requires语句。
  • 一个版本描述符可以有不同的uses语句。

这些规则是基于实现细节的变化是允许的,但是API本身并不是这样。 允许对非JDK模块的requires语句的更改被认为是API中的一个变化 —— 它要求你为不同版本的JDK拥有不同的用户自定义的模块。 这就是为什么这不允许的原因。

模块化MRJAR不需要在根目录中有一个模块描述符。 这是我们在上一节的例子中所说的。 我们在根目录中没有模块描述符,但在META-INF/versions/9目录中有一个描述符。 这种安排可以在一个MRJAR中具有用于JDK 8的非模块化代码和用于JDK 9的模块代码。

三. 模块化多版本JAR和封装

如果在版本目录(在根目录中不存在)中添加新的公共类型,则在创建MRJAR时收到错误。假设在我们的示例中,为JDK 9版本添加一个名为Test的公共类。如果Test类在com.jdojo.mrjar包中,它将被该模块导出,并且可用于MRJAR之外的代码。请注意,根目录不包含Test类,因此此MRJAR为JDK 8和JDK 9提供不同的公共API。在这种情况下,当您创建MRJAR时,在JDK 9的com.jdojo.mrjar包中添加公共Test类将会生成错误。

继续使用相同的示例,假设将Test类添加到JDK 9的com.jdojo.test包中。请注意,该模块不导出此包。当在模块路径上使用此MRJAR时,Test类将无法访问外部代码。在这个意义上,这个MRJAR为JDK 8和JDK 9提供了相同的公共API。但是,有一个隐情!你也可以将此MRJAR放在JDK 9中的类路径上,在这种情况下,外部代码可以访问Test类,但这是对模块化封装的一种违反,以及违反MRJAR应该提供相同公共API的规则跨越不同的JDK版本。因此,不允许在MRJAR中为模块添加公共类型到未导出的包。如果尝试这样做,将收到类似于以下内容的错误消息:

entry: META-INF/versions/9/com/jdojo/test/Test.class, contains a new public class not found in base entries
invalid multi-release jar file mrjars\com.jdojo.mrjar.jar deleted

有时,需要为同一个类库添加更多类型来支持较新版本的JDK。 必须添加这些类型才能支持较新的实现。 可以通过将包级别的私有类型添加到MRJAR中的版本化目录中来实现。 在这个例子中,如果使类非公开类型,可以添加JDK 9的Test类。

四. 多版本JAR和引导加载器

引导加载器不支持多版本JAR,例如,使用-Xbootclasspath / a选项指定MRJAR。支持这个将使得很少需要的功能的引导加载器实现变得复杂化。

五. 相同JDK版本的文件

MRJAR应该在版本目录中包含相同文件的不同版本。 如果资源或类文件在不同的平台版本中是相同的,那么这样一个文件应该被添加到根目录。 目前,如果jar工具在具有相同内容的多版本目录中看到相同的条目,则会发出警告。

让我们看看这个规则的实际效果。 将com.jdojo.mrjar.jdk9\build\目录的内容复制到com.jdojo.mrjar.jdk10\build\classes目录,因此这两个目录具有相同的内容。 运行以下命令创建一个包含JDK版本8,9和10的代码的MRJAR。请注意,版本化目录9和10中的文件将是相同的。 执行命令如下。

C:\Java9Revealed>jar --create --file mrjars\com.jdojo.mrjar.jar
-C com.jdojo.mrjar.jdk8\build\classes .
--release 9 -C com.jdojo.mrjar.jdk9\build\classes .
--release 10 -C com.jdojo.mrjar.jdk10\build\classes .

报出的警告信息为:

Warning: entry META-INF/versions/9/com/jdojo/mrjar/Main.class contains a class that
is identical to an entry already in the jar
Warning: entry META-INF/versions/9/com/jdojo/mrjar/TimeUtil.class contains a class that
is identical to an entry already in the jar

1. 多版本JAR和JAR URL

在MRJAR之前,JAR中的所有资源都存在于根目录中。 当从类加载器(ClassLoader.getResource(“com/jdojo /mrjar/TimeUtil.class”))请求资源时,返回的URL类似于以下内容:

jar:file:/C:/Java9Revealed/mrjars/com.jdojo.mrjar.jar! com/jdojo/mrjar/TimeUtil.class
With MRJARs, a resource may be returned from the root directory or from a versioned directory. If you are looking for the TimeUtil.class file on JDK 9, the URL will be as follows:
jar:file:/C:/Java9Revealed/mrjars/com.jdojo.mrjar.jar!/META-INF/versions/9/com/jdojo/mrjar/TimeUtil.class

如果你现有的代码期望特定格式的资源的jar文件的URL,或者手工编写了了一个URL,当与MRJAR一起工作时,可能会获得意想不到的结果。如果正在使用MRJAR重新打包JAR,则需要再次查看代码并将其更改,能够在MRJAR下工作。

2. 多版本Manifest属性

MRJAR在其MANIFEST.MF文件中包含一个特殊属性:

Multi-Release: true

Multi-Release属性在使用jar工具创建MRJAR时添加。 如果此属性的值为true,则表示JAR是多版本JAR。 如果其值为false或属性缺失,则不是多版本JAR。

名为MULTI_RELEASE的新常量已添加到Attributes.Name类(位于java.util.jar包中),以表示manifest 文件中的新属性Multi-Release。 因此,Attributes.Name.MULTI_RELEASE常量表示Java代码中Multi-Release属性的值。

六. JMOD格式

JDK 9引入了一种称为JMOD的新格式来封装模块。 JMOD文件旨在处理比JAR文件更多的内容类型。 JMOD文件可以打包本地代码,配置文件,本地命令和其他类型的数据。 目前,JMOD格式基于ZIP格式,将在将来会发生变化。 JDK 9模块以JMOD格式打包,可以在编译时和链接时使用。 运行时不支持JMOD格式。 可以在JDK_HOME\jmods目录中找到它们,其中JDK_HOME是安装JDK 9的目录。可以使用JMOD格式打包自己的模块。 JMOD格式的文件具有.jmod扩展名。 例如,名为java.base的平台模块已打包在java.base.jmod文件中。

JMOD文件可以包含本地代码,这在运行时提取和链接有点棘手。 这就是为什么JMOD文件在编译时和链接时都被支持,运行时却不可以。

1. 使用jmod工具

JDK 9附带了一个名为jmod的新工具。 它位于JDK_HOME\bin目录中。 它可以用于创建一个JMOD文件,列出一个JMOD文件的内容,打印一个模块的描述,并记录使用的模块的哈希值。 使用jmod工具的一般语法如下:

jmod <subcommand> <options> <jmod-file>

其中在jmod命令中至少包含以下一个子命令:

  • create
  • extract
  • list
  • describe
  • hash

list和describe子命令不接受任何选项。 <jmod-file>是要创建的JMOD文件或要描述的现有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哈希命令指定的模块路径上的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> | 指定找到散列模块的模块路径。
--os-arch <os-arch> | 指定要记录在module-info.class文件中的操作系统体系结构。
--os-name <os-name> | 指定要记录在module-info.class文件中的操作系统名称。
--version | 打印jmod 工具的版本。
--warn-if-resolved <reason> | 指定一个jmod工具的提示,如果一个模块被解决,发出一个警告。 <reason>的值可能是三种: deprecated, deprecated-for-remova, 或incubating
@<filename> | 从指定的文件中读取选项。

以下部分将详细介绍如何使用jmod命令。 本章中使用的所有命令均输入一行。 有时候,为了显示更加清晰,将它们显示在多行上。

七. 创建JMOD文件

你可以使用jmod工具命令的子命令create来创建一个jmod文件。一个jmod文件的内容是一个模块的内容。假设下列目录和文件已经存在:

  • C:\Java9Revealed\jmods
  • C:\Java9Revealed\lib\com.jdojo.prime.jar

以下命令在C:\ Java9Revealed\jmods目录中创建一个com.jdojo.prime.jmod文件。 JMOD文件的内容来自com.jdojo.prime.jar文件。

C:\Java9Revealed>jmod create --class-path lib\com.jdojo.prime.jar
jmods\com.jdojo.prime.jmod

通常,JMOD文件的内容来自包含模块编译代码的一系列目录。 以下命令创建一个com.jdojo.prime.jmod文件。它的内容来自一个mods\com.jdojo.prime目录。 该命令使用--module-version选项来设置将记录在com.jdojo.prime\build\classes目录中的module-info.class文件中的模块版本。 确保删除在上一步中创建的JMOD文件。

C:\Java9Revealed>jmod create --module-version 1.0
  --class-path com.jdojo.prime\build\classes jmods\com.jdojo.prime.jmod

你可以用这个JMOD文件做什么? 可以将其放在模块路径上,以便在编译时使用它。 可以使用它与jlink工具来创建可用于运行应用程序的自定义运行时映像。 回想一下,你不能在运行时使用它。 如果尝试在运行时使用JMOD文件放在模块路径上,将收到以下错误:

Error occurred during initialization of VM
java.lang.module.ResolutionException: JMOD files not supported: jmods\com.jdojo.prime.jmod

八. 提取JMOD文件内容

可以使用extract子命令提取JMOD文件的内容。 以下命令将jmods\com.jdojo.prime.jmod文件的内容提取到名为extract的目录中。

C:\Java9Revealed>jmod extract --dir extracted jmods\com.jdojo.prime.jmod

如果没有--dir选项,则JMOD文件的内容直接提取到当前目录下。

九. 列出JMOD文件内容

可以使用list子命令与jmod工具打印JMOD文件中所有条目的名称。 以下命令列出了在上一节中创建的com.jdojo.prime.jmod文件的内容:

C:\Java9Revealed>jmod list jmods\com.jdojo.prime.jmod

以下命令列出了java.base.jmod的JMOD文件发布的java.base模块的内容。 该命令假设已经在C:\java9目录中安装了JDK 9。 输出超过120页。 下面显示的是部分输出。 请注意,JMOD文件内部将不同类型的内容存储在不同的目录中。

C:\Java9Revealed>jmod list C:\java9\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工具来描述JMOD文件中包含的模块。 以下命令描述com.jdojo.prime.jmod文件中包含的模块:

C:\Java9Revealed>jmod describe jmods\com.jdojo.prime.jmod

输出结果为:

com.jdojo.prime@1.0
  requires mandated java.base
  uses com.jdojo.prime.PrimeChecker
  exports com.jdojo.prime

可以使用此命令来描述平台模块。 以下命令描述了java.sql.jmod中包含的模块,假设已在C:\java9目录中安装了JDK 9:

C:\Java9Revealed>jmod describe C:\java9\jmods\java.sql.jmod
java.sql@9-ea

输出结果为:

 requires mandated java.base
  requires transitive java.logging
  requires transitive java.xml
  uses java.sql.Driver
  exports java.sql
  exports javax.sql
  exports javax.transaction.xa
  operating-system-name Windows
  operating-system-architecture amd64

十一. 记录模块哈希值

可以使用jmod工具的hash子命令记录其他模块的哈希值,包含在一个jmod文件模块的module-info.class文件中。哈希值将用于以后的依赖性验证。假设你在四个jmod文件中有四个模块:

  • com.jdojo.prime
  • com.jdojo.prime.generic
  • com.jdojo.prime.faster
  • com.jdojo.prime.client

假设希望将这些模块提供给给客户,并确保模块代码保持不变。可以通过为这四个模块记录哈希值来实现。让我们看看如何做到这一点。

如果要计算其他模块的哈希值,首先jmod能够找到这些模块。 你需要使用 --module-path选项在模块路径中,以便找到其他模块。 同时,也需要使用--hash-modules选项来指定要使用的哈希值记录模块的模式列表。

Tips
当你把一个模块打包成一个JAR格式时,可以在jar命令中使用--hash-modules--module-path来记录依赖模块的哈希值。

使用以下四个命令为四个模块创建jmod文件。在创建com.jdojo.prime.client.jmod时使用了--main-class选项。 如果在运行这些命令时,给你一个“file already exists”的错误,从jmods目录删除现有的jmod文件并重新运行命令。

C:\Java9Revealed>jmod create --module-version 1.0
--class-path com.jdojo.prime\build\classes jmods\com.jdojo.prime.jmod
C:\Java9Revealed>jmod create --module-version 1.0
--class-path com.jdojo.prime.generic\build\classes
jmods\com.jdojo.prime.generic.jmod
C:\Java9Revealed>jmod create --module-version 1.0
--class-path com.jdojo.prime.faster\build\classes
jmods\com.jdojo.prime.faster.jmod
C:\Java9Revealed>jmod create --main-class com.jdojo.prime.client.Main
--module-version 1.0
--class-path com.jdojo.prime.client\build\classes
jmods\com.jdojo.prime.client.jmod

现在你准备好记录名字以com.jdojo.prime的模块的哈希值。在com.jdojo.prime模块中使用下面的命令:

C:\Java9Revealed>jmod hash --module-path jmods
  --hash-modules com.jdojo.prime.? jmods\com.jdojo.prime.jmod

让我们看看记录在com.jdojo.prime模块的哈希值。下面命令打印记录有哈希值的模块描述:

C:\Java9Revealed>jmod describe jmods\com.jdojo.prime.jmod

输出结果为:

com.jdojo.prime@1.0
  requires mandated java.base
  uses com.jdojo.prime.PrimeChecker
  exports com.jdojo.prime
  hashes com.jdojo.prime.client SHA-256 2ffb0d4413501e389d6712450bd138bbe82ca8abeb4e8b5d29b0c307d90a2e91
  hashes com.jdojo.prime.faster SHA-256 687e07c429080c48bed89a649dca20fa26dc28fab88a4905f1b5070560622a0c
  hashes com.jdojo.prime.generic SHA-256 f24556ef69c4345ad7a8e5e59d31ea2d52c8749714ede0c0dedf128255450708

当你使用create子命令创建一个新的jmod文件时,也可以记录其他模块的哈希值。假设三个模块com.jdojo.prime.genericcom.jdojo.prime.faster,和com.jdojo.prime.client存在于模块的路径,你可以使用下面的命令来创建com.jdojo.prime.jmod文件同时也记录了其他三个模块的哈希值:

C:\Java9Revealed>jmod create --module-version 1.0
--module-path jmods
--hash-modules com.jdojo.prime.?
--class-path com.jdojo.prime\build\classes jmods\com.jdojo.prime.jmod

可以使用--dry-run选项运行哈希过程的JMOD文件,其中将打印哈希值,但不会记录。 此选项有助于确保所有设置是正确的,而无需创建JMOD文件。 以下的命令顺序将会引导你完成整个过程。 首先,删除在上一步中创建的jmods\com.jdojo.prime.jmod文件。

以下命令创建jmods\com.jdojo.prime.jmod文件,而不会为其他模块记录哈希值:

C:\Java9Revealed>jmod create --module-version 1.0
--module-path jmods
--class-path com.jdojo.prime\build\classes jmods\com.jdojo.prime.jmod

以下命令运行hash --dry-run子命令。 它计算并打印其他模块的哈希值,与--hash-modules选项中指定的正则表达式匹配。 jmods\com.jdojo.prime.jmod文件中不会记录哈希值。

C:\Java9Revealed>jmod hash --dry-run --module-path jmods
 --hash-modules com.jdojo.prime.? jmods\com.jdojo.prime.jmod

输出结果为:

Dry run:
com.jdojo.prime
  hashes com.jdojo.prime.client SHA-256 2ffb0d4413501e389d6712450bd138bbe82ca8abeb4e8b5d29b0c307d90a2e91
  hashes com.jdojo.prime.faster SHA-256 687e07c429080c48bed89a649dca20fa26dc28fab88a4905f1b5070560622a0c
  hashes com.jdojo.prime.generic SHA-256 f24556ef69c4345ad7a8e5e59d31ea2d52c8749714ede0c0dedf128255450708

以下命令验证上一个命令在JMOD文件中没有记录哈任何希值:

C:\Java9Revealed>jmod describe jmods\com.jdojo.prime.jmod

输出结果为:

com.jdojo.prime@1.0
  requires mandated java.base
  uses com.jdojo.prime.PrimeChecker
  exports com.jdojo.prime

十二. 总结

JDK 9支持四种格式来打包模块:展开的目录,JAR文件,JMOD文件和JIMAGE文件。 JAR格式在JDK 9中得到了增强,以支持模块化JAR和多版本JAR。多版本JAR允许你打包相同版本的类库或面向JDK不同版本的应用程序。例如,多版本JAR可能包含包含JDK 8和JDK 9代码的库版本为1.2的代码。当在JDK 8上使用多版本JAR时,将使用JDK 8版本的库代码。当它在JDK 9上使用时,将使用JDK 9版本的库代码。特定于JDK版本N的文件存储在多版本JAR的META-INF\versions\N目录中。所有JDK版本通用的文件都存储在根目录中。对于不支持多版本JAR的环境,此类JAR将被视为常规JAR。在多版本JAR中,文件的搜索顺序是不同的,所有从当前平台的主版本开始的所有版本化目录都在根目录之前进行搜索。

JMOD文件旨在处理比JAR文件更多的内容类型。它们可以打包本地代码,配置文件,本地命令和其他类型的数据。目前,JMOD格式基于ZIP格式,将在将来会发生变化。 JDK 9模块以JMOD格式打包,可以在编译时和链接时使用。运行时不支持JMOD格式。可以使用jmod工具来处理JMOD文件。

posted @ 2017-06-29 18:16  林本托  阅读(7978)  评论(0编辑  收藏  举报