Java 9 揭秘(8. JDK 9重大改变)

Tips
做一个终身学习的人。

Java 9

在本章,主要介绍以下内容:

  • 新的JDK版本控制方案是什么
  • 如何使用Runtime.Version类解析JDK版本字符串
  • JDK JRE 9的新目录布局是什么
  • JDK 9中的批注的标准覆盖机制如何工作的
  • 在JDK 9中使用扩展机制的变化
  • JDK 9中的类加载器如何工作以及模块的加载方式
  • 资源如何封装在JDK 9中的模块中
  • 如何使用ModuleClassClassLoader类中的资源查找方法访问模块中的资源
  • jrt URL方案是什么,以及如何使用它来访问运行时映像中的资源
  • 如何访问JDK 9中的JDK内部API以及JDK 9中已删除的JDK API列表
  • JDK 9中如何使用--patch-module命令行选项替换模块中的类和资源

一. 新的JDK版本控制方案

在JDK 9之前,JDK版本控制方案对开发人员来说并不直观,程序解析并不容易。 看看这两个JDK版本,你不能说出他们之间的微妙差异。 很难回答一个简单的问题:哪个版本包含最新的安全修复程序,JDK 7 Update 55或JDK 7 Update 60? 答案不是很明显的,你可能已经猜到了JDK 7 Update 60。这两个版本都包含相同的安全修复程序。 JDK 8 Update 66,1.8.0_66和JDK 8u66版本有什么区别? 它们代表相同的版本。 在了解版本字符串中包含的详细信息之前,有必要详细了解版本控制方案。 JDK 9试图规范JDK版本控制方案,因此人们很容易理解,易于程序解析,并遵循行业标准版本控制方案。

JDK 9包含一个名为Runtime.Version的静态嵌套类,它表示Java SE平台实现的版本字符串。 它可以用于表示,解析,验证和比较版本字符串。

版本字符串按顺序由以下四个元素组成。 只有第一个是强制性的:

  • 版本号
  • 预发行信息
  • 构建信息
  • 附加信息

以下正则表达式定义版本字符串的格式:

$vnum(-$pre)?(\+($build)?(-$opt)?)?

一个简短版本的字符串由一个版本号码组成,可选地包含预发布信息:

$vnum(-$pre)?

可以使用只包含主版本号“9”的版本字符串。“9.0.1-ea + 154-20170130.07.36am”,包含版本字符串的所有部分。

1. 版本号

版本号是按句点分隔的元素序列。 它可以是任意长度。 其格式如下:

^[1-9][0-9]*(((\.0)*\.[1-9][0-9]*)*)*$

版本号可以由一到四个元素组成,如下所示:

$major.$minor.$security(.$addtionalInfo)

$major元素代表JDK版本的主要版本。 主要版本是递增的,其中包含重要的新功能。 例如,JDK 8的主要版本为8,对于JDK 9为9。当主版本号增加时,版本号中的所有其他部分都将被删除。 例如,如果版本号为9.2.2.1,则主版本号从9增加到10时,新版本号将为10。

$minor元素代表JDK版本的次要版本。 增加一个小的更新版本,例如错误修复,新的垃圾收集器,新的JDK特定的API等。

$security元素表示JDK版本的安全级别更新。 它会增加一个安全更新。 当次要版本号增加时,该元素不会重置。 给定$major$security更高值总是表示更安全的版本。 例如,JDK版本9.1.7与JDK版本9.5.7一样安全,因为两个版本的安全级别是相同的,也就是7。另一个例子,JDK版本9.2.2比9.2.1更安全,因为对于相同的主要版本9,前者的安全级别为2大于后者的安全级别1。

以下规则适用于版本号:

  • 所有元素必须是非负整数。
  • 前三个要素分别被视为主要版本,次要版本和安全级别;其余的(如果存在)被视为附加信息,例如指示补丁发布的数字。
  • 只有主要版本元素是强制性的。
  • 版本号的元素不能包含前导零。 例如,JDK 9的主要版本是9,而不是09。
  • 后面的元素不能为零。 也就是说,版本号不能为9.0.0。 它可以是9,9.2或9.0.x,其中x是正整数。

2. 预发行信息

版本字符串中的$pre元素是预发行标识符,例如早期访问版本的ea,预发行版快照,以及开发人员内部构建版本。 这是可选的。 如果它存在,它以前缀为连字符( - ),并且必须是与正则表达式([a-zA-Z0-9] +)匹配的字母数字字符串)。 以下版本字符串包含9作为版本号,ea作为预发布信息。

9-ea

3. 构建信息

版本字符串中的$build元素是为每个提升的构建增加的构建号。 这是可选的。当版本号的任何部分增加时,它将重置为1。 如果它存在,它加上加号(+),并且必须匹配正则表达式(0 | [1-9] [0-9] *)。 以下版本的字符串包含154作为版本号。

9-EA+154

4. 附加信息

版本字符串中的$opt元素包含其他构建信息,例如内部构建的日期和时间。这是可选的。它是字母和数字,可以包含连字符和句点。 如果它存在,它以前缀为连字符(-),并且必须与正则表达式([-a-zA-Z0-9 \。] +)匹配。 如果$build不存在,则需要在$opt值前加一个加号,后跟连字符(+ -)来指定$opt的值。 例如,在9-ea+132-2016-08-23中,$build为132,$opt为2016-08-23; 在9+-123中,$pre$build缺失,$opt为123。以下版本字符串在其附加信息元素中加入发布的日期和时间:

9-EA+154-20170130.07.36am

5. 解析旧版和新版字符串

JDK版本或者是受限更新版本,其中包括新功能和非安全修补程序,或重要补丁更新,其中仅包括针对安全漏洞的修补程序。 版本字符串包括版本号,包括更新号和构建号。 限制更新版本的编号为20的倍数。重要补丁更新使用奇数,通过将五加倍加到先前的限制更新中,并在需要时添加一个以保持计算结果为奇数。 一个例子是1.8.0_31-b13,它是JDK主版本8的更新31。 它的内部版本号是13。注意,在JDK 9之前,版本字符串始终以1开头。

Tips
解析版本字符串以获取JDK版本的主版本的现有代码可能会在JDK 9中失败,具体取决于其使用的逻辑。 例如,如果逻辑通过跳过第一个元素(以前为1)来查找第二个元素的主版本,逻辑将失败。 例如,如果它从1.8.0返回8,那么它将从9.0.1返回0,在那里你会期望9。

6. 系统属性的版本更改

在JDK 9中,包含JDK版本字符串的系统属性返回的值已更改。 下面表格是这些系统属性及其格式的列表。 $vstr$vnum$pre分别指版本字符串,版本号和预发布信息。

系统属性名称
java.version \(vnum(\-\)pre)?
java.runtime.version $vstr
java.vm.version $vstr
java.specification.version $vnum
java.vm.specification.version $vnum

7. 使用Runtime.Version

DK 9添加了一个名为Runtime.Version的静态嵌套类,其实例代表版本字符串。 Version类没有公共构造函数。 获取其实例的唯一方法是调用静态方法parse(String vstr)。 如果版本字符串为空或无效,该方法可能会抛出运行时异常。

import java.lang.Runtime.Version;
...
// Parse a version string "9.0.1-ea+132"
Version version =  Version.parse("9.0.1-ea+132");

Runtime.Version类中的以下方法返回版本字符串的元素。 方法名称足够直观,可以猜测它们返回的元素值的类型。

int major()
int minor()
int security()
Optional<String> pre()
Optional<Integer> build()
Optional<String> optional()

注意,对于可选元素,$pre$build$opt,返回类型为Optional。 对于可选的$minor$security元素,返回类型为int,而不是Optional,如果版本字符串中缺少$minor$security,则返回零。

回想一下,版本字符串中的版本号可能包含第三个元素之后的附加信息。 Version类不包含直接获取附加信息的方法。 它包含一个version()方法,该方法返回List<Integer>,其中列表包含版本号的所有元素。 列表中的前三个元素是$major$minor$security。 其余元素包含附加版本号信息。

Runtime.Version类包含在次序和等式方面比较两个版本字符串的方法。 可以比较它们或者不包含可选的构建信息($opt)。 这些比较方法如下:

int compareTo(Version v)
int compareToIgnoreOptional(Version v)
boolean equals(Object v)
boolean equalsIgnoreOptional(Object v)

如果v1小于等于或大于v2,表达式v1.compareTo(v2)将返回负整数,零或正整数。 compareToIgnoreOptional()方法的工作方式与compareTo()方法相同,只不过它在比较时忽略了可选的构建信息。 equals()equalsIgnoreOptional()方法将两个版本字符串进行比较,不包含可选构建信息。

哪个版本的字符串代表最新版本:9.1.1或9.1.1-ea? 第一个不包含预发行元素,而第二个字符串包含,所以第一个是最新版本。 哪个版本的字符串代表最新版本:9.1.1或9.1.1.1-ea? 这一次,第二个代表最新的版本。 比较发生在序列$vnum$pre$build$opt。 当版本号较大时,不比较版本字符串中的其他元素。

此部分的源代码位于名为com.jdojo.version.string的模块中,其声明如下所示。

// module-info.java
module com.jdojo.version.string {
    exports com.jdojo.version.string;
}

下面代码包含一个完整的程序,显示如何使用Runtime.Version类来提取版本字符串的所有部分。

com.jdojo.version.string
// VersionTest.java
package com.jdojo.version.string;
import java.util.List;
import java.lang.Runtime.Version;
public class VersionTest {
    public static void main(String[] args) {
        String[] versionStrings = {
            "9", "9.1", "9.1.2", "9.1.2.3.4", "9.0.0",
            "9.1.2-ea+153", "9+132", "9-ea+132-2016-08-23", "9+-123",
            "9.0.1-ea+132-2016-08-22.10.56.45am"};
        for (String versonString : versionStrings) {
            try {
                Version version = Version.parse(versonString);
                // Get the additional version number elements
                // which start at 4th element
                String vnumAdditionalInfo = getAdditionalVersionInfo(version);
                System.out.printf("Version String=%s%n", versonString);
                System.out.printf("Major=%d, Minor=%d, Security=%d, Additional Version=%s,"
                        + " Pre=%s, Build=%s, Optional=%s %n%n",
                        version.major(),
                        version.minor(),
                        version.security(),
                        vnumAdditionalInfo,
                        version.pre().orElse(""),
                        version.build().isPresent() ? version.build().get().toString() : "",
                        version.optional().orElse(""));
            } catch (Exception e) {
                System.out.printf("%s%n%n", e.getMessage());
            }
        }
    }
    // Returns the version number elements from the 4th elements to the end
    public static String getAdditionalVersionInfo(Version v) {
        String str = "";
        List<Integer> vnum = v.version();
        int size = vnum.size();
        if (size >= 4) {
            str = str + String.valueOf(vnum.get(3));
        }
        for (int i = 4; i < size; i++) {
            str = str + "." + String.valueOf(vnum.get(i));
        }
        return str;
    }
}

VersionTest类,显示如何使用Runtime.Version类来处理版本字符串。
下面是输出结果:

Version String=9
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=, Optional=
Version String=9.1
Major=9, Minor=1, Security=0, Additional Version=, Pre=, Build=, Optional=
Version String=9.1.2
Major=9, Minor=1, Security=2, Additional Version=, Pre=, Build=, Optional=
Version String=9.1.2.3.4
Major=9, Minor=1, Security=2, Additional Version=3.4, Pre=, Build=, Optional=
Invalid version string: '9.0.0'
Version String=9.1.2-ea+153
Major=9, Minor=1, Security=2, Additional Version=, Pre=ea, Build=153, Optional=
Version String=9+132
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=132, Optional=
Version String=9-ea+132-2016-08-23
Major=9, Minor=0, Security=0, Additional Version=, Pre=ea, Build=132, Optional=2016-08-23
Version String=9+-123
Major=9, Minor=0, Security=0, Additional Version=, Pre=, Build=, Optional=123
Version String=9.0.1-ea+132-2016-08-22.10.56.45am
Major=9, Minor=0, Security=1, Additional Version=, Pre=ea, Build=132, Optional=2016-08-22.10.56.45am

二. JDK和JRE的改变

JDK和JRE已经在Java SE 9中进行了模块化处理。对结构进行了一些修改。 还进行了一些其他更改,以提高性能,安全性和可维护性。 大多数这些变化会影响类库开发人员和IDE开发人员,而不是应用程序开发人员。为了讨论这些变化,把它们分为三大类:

  • 布局变化
  • 行为变化
  • API更改

以下部分将详细介绍这些改变。

1. JDK和JRE的布局变化

结构更改会影响运行时映像中的目录和文件的组织方式,并影响其内容。 在Java SE 9之前,JDK构建系统用于生成两种类型的运行时映像 ——Java运行时环境(JRE)和Java开发工具包(JDK)。 JRE是Java SE平台的完整实现,JDK包含了JRE和开发工具和类库。 可下图显示了Java SE 9之前的JDK安装中的主目录。JDK_HOME是安装JDK的目录。 如果你只安装了JRE,那么你只有在jre目录下的目录。

Java SE 9之前的JDK和JRE目录布局

在 Java SE 9之前,JDK中:

  • bin目录用于包含命令行开发和调试工具,如javac,jar和javadoc。 它还用于包含Java命令来启动Java应用程序。
  • include目录包含在编译本地代码时使用的C/C++头文件。
  • lib目录包含JDK工具的几个JAR和其他类型的文件。 它有一个tools.jar文件,其中包含javac编译器的Java类。
  • jre\bin目录包含基本命令,如java命令。 在Windows平台上,它包含系统的运行时动态链接库(DLL)。
  • jre\lib目录包含用户可编辑的配置文件,如.properties和.policy文件。
  • jre\lib\approved目录包含允许使用标准覆盖机制的JAR。 这允许在Java社区进程之外创建的实施标准或独立技术的类和接口的更高版本被并入Java平台。 这些JAR被添加到JVM的引导类路径中,从而覆盖了Java运行时中存在的这些类和接口的任何定义。
  • jre\lib\ext目录包含允许扩展机制的JAR。 该机制通过扩展类加载器(该类加载器)加载了该目录中的所有JAR,该引导类加载器是系统类加载器的子进程,它加载所有应用程序类。 通过将JAR放在此目录中,可以扩展Java SE平台。 这些JAR的内容对于在此运行时映像上编译或运行的所有应用程序都可见。
  • jre\lib目录包含几个JAR。 rt.jar文件包含运行时的Java类和资源文件。 许多工具依赖于rt.jar文件的位置。
  • jre\lib目录包含用于非Windows平台的动态链接本地库。
  • jre\lib目录包含几个其他子目录,其中包含运行时文件,如字体和图像。

JDK和JRE的根目录包含多个文件,如COPYRIGHT,LICENSE和README.html。 根目录中的发行文件包含一个描述运行时映像(如Java版本,操作系统版本和体系结构)的键值对。 以下代码显示了JDK 8中的示例版本文件的部分内容:

JAVA_VERSION="1.8.0_66"
OS_NAME="Windows"
OS_VERSION="5.2"
OS_ARCH="amd64"
BUILD_TYPE="commercial"

Java SE 9调整了JDK的目录层次结构,并删除了JDK和JRE之间的区别。 下图显示了Java SE 9中JDK安装的目录。JDK 9中的JRE安装不包含include和jmods目录。

Java SE 9中的JDK目录布局

在Java SE 9 的JDK中:

  • 没有名为jre的子目录。
  • bin目录包含所有命令。 在Windows平台上,它继续包含系统的运行时动态链接库。
  • conf目录包含用户可编辑的配置文件,例如以前位于jre\lib目录中的.properties和.policy文件。
  • include目录包含要在以前编译本地代码时使用的C/C++头文件。 它只存在于JDK中。
  • jmods目录包含JMOD格式的平台模块。 创建自定义运行时映像时需要它。 它只存在于JDK中。
  • legal 目录包含法律声明。
  • lib目录包含非Windows平台上的动态链接本地库。 其子目录和文件不应由开发人员直接编辑或使用。

JDK 9的根目录有如COPYRIGHT和README等文件。 JDK 9中的发行文件包含一个带有MODULES键的新条目,其值为映像中包含的模块列表。 JDK 9映像中的发行文件的部分内容如下所示:

MODULES=java.rmi,jdk.jdi,jdk.policytool
OS_VERSION="5.2"
OS_ARCH="amd64"
OS_NAME="Windows"
JAVA_VERSION="9"
JAVA_FULL_VERSION="9-ea+133"

在列表中只显示了三个模块。 在完整的JDK安装中,此列表将包括所有平台模块。 在自定义运行时映像中,此列表将仅包含你在映像中使用的模块。

Tips
JDK中的lib\tools.jar和JRE中的lib\rt.jar已从Java SE 9中删除。这些JAR中可用的类和资源现在以文件中的内部格式存储在lib目录的命名模块中。 可以使用称为jrt的新方案来从运行时映像检索这些类和资源。 依靠这些JAR位置的应用程序将不再工作。

2. 行为变化

行为变化将影响应用程序的运行时行为。 以下部分将说明这些更改。

三. 支持标准覆盖机制

在Java SE 9之前,可以使用支持标准的覆盖机制来使用更新版本的类和接口来实现支持标准或独立API,如javax.rmi.CORBA包和Java API for XML Processing(JAXP) ,它们是在Java社区进程之外创建的。 这些JAR已经被添加到JVM的引导类路径中,从而覆盖了JRE中存在的这些类和接口的任何定义。 这些JAR的位置由名为java.endorsed.dirs的系统属性指定,其中目录由特定于平台的路径分隔符字符分隔。 如果未设置此属性,则运行时将在jre\lib\approved目录中查找JAR。

Java SE 9仍然支持认可的标准和独立API覆盖机制。 在Java SE 9中,运行时映像由模块组成。 要使用此机制,需要使用更新版本的模块,用于支持标准和独立API。 需要使用--upgrade-module-path命令行选项。 此选项的值是包含“承认标准”和“独立API”模块的目录列表。 Windows上的以下命令将覆盖“标准标准”模块,如JDK 9中的java.corba模块。将使用umod1和umod2目录中的模块而不是运行时映像中的相应模块:

java --upgrade-module-path umod1;umod2 <other-options>

Tips
在Java SE 9中,创建一个JAVA_HOME\lib\approvaled目录并设置名为java.endorsed.dirs的系统属性,会产生错误。

四. 扩展机制

版本9之前的Java SE允许扩展机制,可以通过将JAR放置在系统属性java.ext.dirs指定的目录中来扩展运行时映像。 如果未设置此系统属性,则使用jre\lib\ext目录作为其默认值。 该机制通过扩展类加载器(这是引导类加载器的子类)和系统类加载器的父级加载了该目录中的所有JAR。 它加载所有应用程序类。 这些JAR的内容对于在此运行时映像上编译或运行的所有应用程序都可见。

Java SE 9不支持扩展机制。 如果需要类似的功能,可以将这些JAR放在类路径的前面。 使用名为JAVA_HOME\lib\ext的目录或设置名为java.ext.dirs的系统属性会导致JDK 9中的错误。

1. 类加载器的改变

在程序运行时,每个类型都由类加载器加载,该类由java.lang.ClassLoader类的一个实例表示。 如果你有一个对象引用obj,你可以通过调用obj.getClass().getClassLoader()方法获得它的类加载器引用。 可以使用其getParent()方法获取类加载器的父类。

在版本9之前,JDK使用三个类加载器来加载类,如下图所示。 图中箭头方向表示委托方向。 可以添加更多的类加载器,这是ClassLoader类的子类。 来自不同位置和类型的JDK加载类中的三个类加载器。

版本9之前的JDK中的类加载器层次结构

JDK类加载器以分层方式工作 —— 引导类加载器位于层次结构的顶部。 类加载器将类加载请求委托给上层类加载器。 例如,如果应用程序类加载器需要加载一个类,它将请求委托给扩展类加载器,扩展类加载器又将请求委托给引导类加载器。 如果引导类加载器无法加载类,扩展类加载器将尝试加载它。 如果扩展类加载器无法加载类,则应用程序类加载器尝试加载它。 如果应用程序类加载器无法加载它,则抛出ClassNotFoundException异常。

引导类加载器是扩展类加载器的父类。 扩展类加载器是应用程序类加载器的父类。 引导类加载器没有父类。 默认情况下,应用程序类加载器将是你创建的其他类加载器的父类。

引导类加载器加载由Java平台组成的引导类,包括JAVA_HOME\lib\rt.jar中的类和其他几个运行时JAR。 它完全在虚拟机中实现。 可以使用-Xbootclasspath/p-Xbootclasspath/a命令行选项来附加引导目录。 可以使用-Xbootclasspath选项指定引导类路径,该选项将替换默认的引导类路径。 在运行时,sun.boot.class.path系统属性包含引导类路径的只读值。 JDK通过null表示这个类加载器。 也就是说,你不能得到它的引用。 例如,Object类由引导类加载器加载,并且Object.class.getClassLoade()表达式将返回null。

扩展类加载器用于通过java.ext.dirs系统属性指定的目录中的位于JAR中的扩展机制加载可用的类。要获得扩展类加载器的引用,需要获取应用程序类加载器的引用,并在该引用上使用getParent()方法。

应用程序类加载器从由CLASSPATH环境变量指定的应用程序类路径或命令行选项-cp-classpath加载类。应用程序类加载器也称为系统类加载器,这是一种误称,它暗示它加载系统类。可以使用ClassLoader类的静态方法getSystemClassLoader()获取对应用程序类加载器的引用。

JDK 9保持三级分层类加载器架构以实现向后兼容。但是,从模块系统加载类的方式有一些变化。 JDK 9类加载器层次结构如下图所示。

JDK 9中的加载器层次结构

请注意,在JDK 9中,应用程序类加载器可以委托给平台类加载器以及引导类加载器;平台类加载器可以委托给引导类加载器和应用程序类加载器。 以下详细介绍JDK 9类加载器的工作原理。

在JDK 9中,引导类加载器是由类库和代码在虚拟机中实现的。 为了向后兼容,它在程序中仍然由null表示。 例如,Object.class.getClassLoader()仍然返回null。 但是,并不是所有的Java SE平台和JDK模块都由引导类加载器加载。 举几个例子,引导类加载器加载的模块是java.basejava.loggingjava.prefsjava.desktop。 其他Java SE平台和JDK模块由平台类加载器和应用程序类加载器加载,这在下面介绍。 JDK 9中不再支持用于指定引导类路径,-Xbootclasspath-Xbootclasspath/p选项以及系统属性sun.boot.class.path-Xbootclasspath/a选项仍然受支持,其值存储在jdk.boot.class.path.append的系统属性中。

JDK 9不再支持扩展机制。 但是,它将扩展类加载器保留在名为平台类加载器的新名称下。 ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。 下表包含平台类加载器加载的模块列表。 平台类加载器用于另一目的。 默认情况下,由引导类加载器加载的类将被授予所有权限。 但是,几个类不需要所有权限。 这些类在JDK 9中已经被取消了特权,并且它们被平台类加载器加载以提高安全性。

下面是JDK 9中由平台加载器加载的模块列表。

java.activation
java.xml.ws.annotation
jdk.desktop
java.compiler
javafx.base
jdk.dynalink
java.corba
javafx.controls
jdk.javaws
java.jnlp
javafx.deploy
jdk.jsobject
java.scripting
javafx.fxml
jdk.localedata
java.se
javafx.graphics
jdk.naming.dns
java.se.ee
javafx.media
jdk.plugin
java.security.jgss
javafx.swing
jdk.plugin.dom
java.smartcardio
javafx.web
jdk.plugin.server
java.sql
jdk.accessibility
jdk.scripting.nashorn
java.sql.rowset
jdk.charsets
jdk.security.auth
java.transaction
jdk.crypto.cryptoki
jdk.security.jgss
java.xml.bind
jdk.crypto.ec
jdk.xml.dom
java.xml.crypto
jdk.crypto.mscapi
jdk.zipfs
java.xml.ws
jdk.deploy

应用程序类加载器加载在模块路径上找到的应用程序模块和一些提供工具或导出工具API的JDK模块,如下表所示。 仍然可以使用ClassLoader类的getSystemClassLoader()的静态方法来获取应用程序类加载器的引用。

jdk.attach
jdk.jartool
jdk.jstatd
jdk.compiler
jdk.javadoc
jdk.pack
jdk.deploy.controlpanel
jdk.jcmd
jdk.packager
jdk.editpad
jdk.jconsole
jdk.packager.services
jdk.hotspot.agent
jdk.jdeps
jdk.policytool
jdk.internal.ed
jdk.jdi
jdk.rmic
jdk.internal.jvmstat
jdk.jdwp.agent
jdk.scripting.nashorn.shell
jdk.internal.le
jdk.jlink
jdk.xml.bind
jdk.internal.opt
jdk.jshell
jdk.xml.ws

Tips
在JDK 9之前,扩展类加载器和应用程序类加载器是java.net.URLClassLoader类的一个实例。 在JDK 9中,平台类加载器(以前的扩展类加载器)和应用程序类加载器是内部JDK类的实例。 如果你的代码依赖于·URLClassLoader·类的特定方法,代码可能会在JDK 9中崩溃。

JDK 9中的类加载机制有所改变。 三个内置的类加载器一起协作来加载类。 当应用程序类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果有合适的模块定义在这些类加载器中,则该类加载器将加载类,这意味着应用程序类加载器现在可以委托给引导类加载器和平台类加载器。 如果在为这些类加载器定义的命名模块中找不到类,则应用程序类加载器将委托给其父类,即平台类加载器。 如果类尚未加载,则应用程序类加载器将搜索类路径。 如果它在类路径中找到类,它将作为其未命名模块的成员加载该类。 如果在类路径中找不到类,则抛出ClassNotFoundException异常。

当平台类加载器需要加载类时,它将搜索定义到所有类加载器的模块。 如果一个合适的模块被定义为这些类加载器中,则该类加载器加载该类。 这意味着平台类加载器可以委托给引导类加载器以及应用程序类加载器。 如果在为这些类加载器定义的命名模块中找不到一个类,那么平台类加载器将委托给它的父类,即引导类加载器。

当引导类加载器需要加载一个类时,它会搜索自己的命名模块列表。 如果找不到类,它将通过命令行选项-Xbootclasspath/a指定的文件和目录列表进行搜索。 如果它在引导类路径上找到一个类,它将作为其未命名模块的成员加载该类。

你可以看到类加载器及其加载的模块和类。 JDK 9包含一个名为-Xlog::modules的选项,用于在虚拟机加载时记录调试或跟踪消息。 其格式如下:

-Xlog:modules=<debug|trace>

此选项产生大量的输出。 建议将输出重定向到一个文件,以便可以轻松查看。 以下命令在Windows上运行素数检查的客户端程序,并在test.txt文件中记录模块加载信息。 下面显示部分输出。 输出显示定义模块的类加载器。
命令:

C:\Java9Revealed>java -Xlog:modules=trace --module-path lib
 --module com.jdojo.prime.client/com.jdojo.prime.client.Main > test.txt

部分信息输出:

[0.022s][trace][modules] Setting package: class: java.lang.Object, package: java/lang, loader: <bootloader>, module: java.base
[0.022s][trace][modules] Setting package: class: java.io.Serializable, package: java/io, loader: <bootloader>, module: java.base
...
[0.855s][debug][modules] define_module(): creation of module: com.jdojo.prime.client, version: NULL, location: file:///C:/Java9Revealed/lib/com.jdojo.prime.client.jar, class loader 0x00000049ec86dd90 a 'jdk/internal/loader/ClassLoaders$AppClassLoader'{0x00000000895d1c98}, package #: 1
[0.855s][trace][modules] define_module(): creation of package com/jdojo/prime/client for module com.jdojo.prime.client
...

五. 访问资源

资源是应用程序使用的数据,例如图像,音频,视频,文本文件等。Java提供了一种通过在类路径上定位资源来访问资源的位置无关的方式。 需要以与在JAR中打包类文件相同的方式打包资源,并将JAR添加到类路径。 通常,类文件和资源打包在同一个JAR中。 访问资源是每个Java开发人员执行的重要任务。 在接下来的章节中,将在版本9和JDK 9之前解释JDK中提供可用的API。

1. 在JDK 9之前访问资源

在本节中,将解释如何在版本9之前在JDK中访问资源。如果你已经知道如何在版本9之前访问JDK中的资源,可以跳到下一节,介绍如何访问JDK 9中的资源。

在Java代码中,资源由资源名称标识,资源名称是由斜线(/)分隔的一串字符串。 对于存储在JAR中的资源,资源名称仅仅是存储在JAR中的文件的路径。 例如,在JDK 9之前,存储在rt.jar中的java.lang包中的Object.class文件是一个资源,其资源名称是java/lang/Object.class。

在JDK 9之前,可以使用以下两个类中的方法来访问资源:

java.lang.Class
java.lang.ClassLoader

资源由ClassLoader定位。 一个Class代理中的资源寻找方法到它的ClassLoader。 因此,一旦了解ClassLoader使用的资源加载过程,将不会在使用Class类的方法时遇到问题。 在两个类中有两种不同的命名实例方法:

URL getResource(String name)
InputStream getResourceAsStream(String name)

两种方法都会以相同的方式找到资源。 它们的差异仅在于返回类型。 第一个方法返回一个URL,而第二个方法返回一个InputStream。 第二种方法相当于调用第一种方法,然后在返回的URL对象上调用openStream()

Tips
如果找不到指定的资源,所有资源查找方法都将返回null。

ClassLoader类包含三个额外的查找资源的静态方法:

static URL getSystemResource(String name)
static InputStream getSystemResourceAsStream(String name)
static Enumeration<URL> getSystemResources(String name)

这些方法使用系统类加载器(也称为应用程序类加载器)来查找资源。 第一种方法返回找到的第一个资源的URL。 第二种方法返回找到的第一个资源的InputStream。 第三种方法返回使用指定的资源名称找到的所有资源的URL枚举。

要找到资源,有两种类型的方法可以从——getSystemResource *getResource *中进行选择。 在讨论哪种方法是最好的之前,重要的是要了解有两种类型的资源:

  • 系统资源
  • 非系统资源

你必须了解他们之间的区别,以了解资源查找机制。系统资源是在bootstrap类路径,扩展目录中的JAR和应用程序类路径中找到的资源。非系统资源可以存储在除路径之外的位置,例如在特定目录,网络上或数据库中。 getSystemResource()方法使用应用程序类加载程序找到一个资源,委托给它的父类,它是扩展类加载器,后者又委托给它的父类(引导类加载器)。如果你的应用程序是独立的应用程序,并且它只使用三个内置的JDK类加载器,那么你将很好的使用名为getSystemResource *的静态方法。它将在类路径中找到所有资源,包括运行时映像中的资源,如rt.jar文件。如果你的应用程序是在浏览器中运行的小程序,或在应用程序服务器和Web服务器中运行的企业应用程序,则应使用名为getResource*的实例方法,它可以使用特定的类加载器来查找资源。如果在Class对象上调用getResource*方法,则会使用当前类加载器(加载Class对象的类加载器)来查找资源。

传递给ClassLoader类中所有方法的资源名称都是绝对的,它们不以斜线(/)开头。 例如,当调用ClassLoadergetSystemResource()方法时,将使用java/lang/Object.class作为资源名称。

Class类中的资源查找方法可以指定绝对和相对资源名称。 绝对资源名称以斜线开头,而相对资源名称不用。 当使用绝对名称时,Class类中的方法会删除前导斜线并委派给加载Class对象的类加载器来查找资源。 以下调用

Test.class.getResource("/resources/test.config");
会被转换成
Test.class.getClassLoader().getResource("resources/test.config");

当使用相对名称时,Class类中的方法预先添加了包名称,在使用斜线后跟斜线替换包名中的点,然后再委托加载Class对象的类加载器来查找资源。 假设测试类在com.jdojo.test包中,以下调用:
Test.class.getResource("resources/test.config");
会被转换成
Test.class.getClassLoader() .getResource("com/jdojo/test/resources/test.config");

我们来看一个在JDK 9之前查找资源的例子。 使用JDK 8运行示例。NetBeans项目名为com.jdojo.resource.preJDK9。 如果你创建自己的项目,请确保将项目的Java平台和源更改为JDK 8。类和资源的排列如下:
word_to_number.properties
com/jdojo/resource/prejdk9/ResourceTest.class
com/jdojo/resource/prejdk9/resources/number_to_word.properties

该项目包含两个资源文件:根目录下的word_to_number.properties和com/jdojo/resource/prejdk9/resources目录中的number_to_word.properties。 这两个属性文件的内容分别如下所示:

One=1
Two=2
Three=3
Four=4
Five=5
1=One
2=Two
3=Three
4=Four
5=Five

下面包含一个完整的程序,显示如何使用不同的类及其方法查找资源。 该程序演示了可以将应用程序中的类文件用作资源,可以使用相同的方法找到它们来查找其他类型的资源。

// ResourceTest.java
package com.jdojo.resource.prejdk9;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
public class ResourceTest {
    public static void main(String[] args) {
        System.out.println("Finding resources using the system class loader:");
        findSystemResource("java/lang/Object.class");
        findSystemResource("com/jdojo/resource/prejdk9/ResourceTest.class");
        findSystemResource("com/jdojo/prime/PrimeChecker.class");
        findSystemResource("sun/print/resources/duplex.png");
        System.out.println("\nFinding resources using the Class class:");
        // A relative resource name - Will not find Object.class
        findClassResource("java/lang/Object.class");
        // An absolute resource name - Will find Object.class
        findClassResource("/java/lang/Object.class");
        // A relative resource name - will find the class
        findClassResource("ResourceTest.class");
        // Load the wordtonumber.properties file
        loadProperties("/wordtonumber.properties");
        // Will not find the properties because we are using
        // an absolute resource name
        loadProperties("/resources/numbertoword.properties");
        // Will find the properties
        loadProperties("resources/numbertoword.properties");
    }
    public static void findSystemResource(String resource) {
        URL url = ClassLoader.getSystemResource(resource);
        System.out.println(url);
    }
    public static URL findClassResource(String resource) {
        URL url = ResourceTest.class.getResource(resource);
        System.out.println(url);
        return url;
    }
    public static Properties loadProperties(String resource) {
        Properties p1 = new Properties();
        URL url = ResourceTest.class.getResource(resource);
        if (url == null) {
            System.out.println("Properties not found: " + resource);
            return p1;
        }
        try {
            p1.load(url.openStream());
            System.out.println("Loaded properties from " + resource);
            System.out.println(p1);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
        return p1;
    }
}

以下是输出结果:

Finding resources using the system class loader:
jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class
file:/C:/Java9Revealed/com.jdojo.resource.prejdk9/build/classes/com/jdojo/resource/prejdk9/ResourceTest.class
null
jar:file:/C:/java8/jre/lib/resources.jar!/sun/print/resources/duplex.png
Finding resources using the Class class:
null
jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class
file:/C:/Java9Revealed/com.jdojo.resource.prejdk9/build/classes/com/jdojo/resource/prejdk9/ResourceTest.class
Loaded properties from /wordtonumber.properties
{One=1, Three=3, Four=4, Five=5, Two=2}
Properties not found: /resources/numbertoword.properties
Loaded properties from resources/numbertoword.properties
{5=Five, 4=Four, 3=Three, 2=Two, 1=One}

2. 在JDK 9 中访问资源

在JDK 9之前,可以从类路径上的任何JAR访问资源。 在JDK 9中,类和资源封装在模块中。 在第一次尝试中,JDK 9设计人员强制执行模块封装规则,模块中的资源必须对该模块是私有的,因此它们只能在该模块内的代码中访问。 虽然这个规则在理论上看起来很好,但是对于跨模块共享资源的框架和加载的类文件作为来自其他模块的资源,就会带来问题。 为了有限地访问模块中的资源,做了一些妥协,但是仍然强制执行模块的封装。 JDK 9包含三类资源查找方法:

java.lang.Class
java.lang.ClassLoader
java.lang.Module

ClassClassLoader类没新增任何新的方法。 Module类包含一个getResourceAsStream(String name)方法,如果找到该资源,返回一个InputStream;否则返回null。

六. 资源命名语法

资源使用由斜线分隔的字符串序列命名,例如com/jdojo/states.png,/com/jdojo/words.png和logo.png。 如果资源名称以斜线开头,则被视为绝对资源名称。

使用以下规则从资源名称中估算包(package)的名称:

  • 如果资源名称以斜线开头,删除第一个斜线。 例如,对于资源名称/com/jdojo/words.png,此步骤将导致com/jdojo/words.png。
  • 从最后一个斜线开始删除资源名称中的所有字符。 在这个例子中,com/jdojo/words.png导致com/jdojo。
  • 用点号(.)替换名称中的每个剩余的斜线。 所以,com/jdojo被转换成com.jdojo。 生成的字符串是包名称。

有些情况下使用这些步骤会导致一个未命名的包或一个无效的包名称。 包名称(如果存在)必须由有效的Java标识符组成。 如果没有包名称,它被称为未命名的包。 例如,将META-INF/resource /logo.png视为资源名称。 应用上一组规则,其包名称将被计算为“META-INF.resources”,它不是有效的包名,但它是资源的有效路径。

七. 查找资源的规则

由于向后兼容性和对模块系统的强封装的承诺,JDK 9中查找资源的新规则是复杂的,基于以下几个因素:

  • 包含资源的模块类型:命名的,开放的,未命名的或自动命名的模块;
  • 正在访问资源的模块:它是同一个模块还是另一个模块?
  • 正在被访问的资源的包名称:它是否是有效Java包? 这是一个未命名的包?
  • 封装包含资源的包:将包含资源的包导出,打开或封装到访问资源的模块?
  • 正在访问的资源的文件扩展名:资源是.class文件还是其他类型的文件?
  • 正在使用哪种类的方法来访问资源:ClassClassLoaderModule类?

以下规则适用于包含资源的命名模块:

  • 如果资源名称以.class结尾,则可以通过任何模块中的代码访问资源。 也就是说,任何模块都可以访问任何命名模块中的类文件。
  • 如果从资源名称计算的包名称不是有效的Java包名称,例如META-INF.resources,则可以通过任何模块中的代码访问该资源。
  • 如果从资源名称计算的包名称是未命名的包,例如对于资源名称(如word.png),则可以通过任何模块中的代码访问该资源。
  • 如果包含该资源的软件包对访问该资源的模块开放,则资源可以通过该模块中的代码访问。 一个包对模块开放,因为定义包的模块是一个开放的模块,或者模块打开所有其他模块的包,或者模块只使用一个限定的打开语句打开包。 如果没有以任何这些方式打开包,则该包中的资源不能被该模块外的代码访问。
  • 这个规则是上一个规则的分支。 打开未命名,自动或开放模块中的每个包,因此所有其他模块中的代码都可以访问这些模块中的所有资源。

Tips
命名模块中的包必须打开,而不是导出,以访问其资源。 导出一个模块的包允许其他模块访问该包中的公共类型(而不是资源)。

在访问命名模块中的资源时,ModuleClassClassLoader类中的各种资源查找方法的行为有所不同:

  • 可以使用Module类的getResourceAsStream()方法来访问模块中的资源。 此方法是调用方敏感的。 如果调用者模块不同,则此方法将应用所有资源可访问性规则,如上所述。
  • 在指定模块中定义的类的Class类中的getResource *()方法仅在该命名模块中定位资源。 也就是说,不能使用这些方法来定位定义调用这些方法的类的命名模块之外的类。
  • ClassLoader类中的getResource *()方法基于前面描述的规则列表来定位命名模块中的资源。 这些方法不是调用者敏感的。 在尝试查找资源本身之前,类加载器将资源搜索委托给其父类。 这些方法有两个例外:1)它们仅在无条件打开的包中定位资源。 如果使用限定的打开语句打开包,则这些方法将不会在这些包中找到资源。 2)它们搜索在类加载器中定义的模块。

Class对象将仅在它所属的模块中找到资源。 它还支持以斜线开头的绝对资源名称,以及不以斜线开头的相对资源名称。 以下是使用Class对象的几个示例:

// Will find the resource
URL url1 = Test.class.getResource("Test.class");
// Will not find the resource because the Test and Object classes are in different modules
URL url2 = Test.class.getResource("/java/lang/Object.class");
// Will find the resource because the Object and Class classes are in the same module, java.base
URL url3 = Object.class.getResource("/java/lang/Class.class");
// Will not find the resource because the Object class is in the java.base module whereas
// the Driver class is in the java.sql module
URL url4 = Object.class.getResource("/java/sql/Driver.class");

使用Module类定位资源需要具有该模块的引用。 如果可以访问该模块中的类,则在该Class对象上使用getModule()方法给出了模块引用。 这是获取模块引用的最简单方法。 有时候,你把模块名称作为字符串,而不是该模块中的类的引用。 可以从模块名称中找到模块引用。 模块被组织成由java.lang包中的ModuleLayer类的实例表示的层。 JVM至少包含一个boot 层。 boot层中的模块映射到内置的类加载器 —— 引导类加载器,平台类加载器和应用程序类加载器。 可以使用ModuleLayer类的boot()静态方法获取boot层的引用:

// Get the boot layer
ModuleLayer bootLayer = ModuleLayer.boot();

一旦获得boot层的引用,可以使用其findModule(String moduleName)方法获取模块的引用:

// Find the module named com.jdojo.resource in the boot layer
Optional<Module> m = bootLayer.findModule("com.jdojo.resource");
// If the module was found, find a resource in the module
if(m.isPresent()) {
    Module testModule = m.get();
    String resource = "com/jdojo/resource/opened/opened.properties";
    InputStream input = module.getResourceAsStream(resource);
    if (input != null) {
        System.out.println(resource + " found.");
    } else {
        System.out.println(resource + " not found.”);
    }
} else {
    System.out.println("Module com.jdojo.resource does not exist");
}

八. 访问命名模块中的资源的示例

在本部分中,将看到资源查找规则的具体过程。 在com.jdojo.resource的模块中打包资源,其声明如下所示。

// module-info.java
module com.jdojo.resource {
    exports com.jdojo.exported;
    opens com.jdojo.opened;
}

该模块导出com.jdojo.exported包,并打开com.jdojo.opened包。

以下是com.jdojo.resource模块中所有文件的列表:

  • module-info.class
  • unnamed.properties
  • META-INF\invalid_pkg.properties
  • com\jdojo\encapsulated\encapsulated.properties
  • com\jdojo\encapsulated\EncapsulatedTest.class
  • com\jdojo\exported\AppResource.class
  • com\jdojo\exported\exported.properties
  • com\jdojo\opened\opened.properties
  • com\jdojo\opened\OpenedTest.class

有四个类文件。 在这个例子中,只有module-info.class文件很重要。 其他类文件定义一个没有任何细节的同名的类。 具有.properties扩展名的所有文件都是资源文件,其内容在此示例中不重要。 源代码包含Java9Revealed\com.jdojo.resource目录中这些文件的内容。

Tips
源代码在com.jdojo.resource

unnamed.properties文件在未命名的包中,因此可以通过任何其他模块中的代码来定位。 invalid_pkg.properties文件位于META-INF目录中,它不是有效的Java包名称,因此该文件也可以通过任何其他模块中的代码来定位。 com.jdojo.encapsulated包没有打开,所以encapsulated.properties文件不能通过其他模块中的代码来找到。 com.jdojo.exported包未打开,所以export.properties文件不能通过其他模块中的代码来找到。 com.jdojo.opened包是打开的,所以opened.properties文件可以通过其他模块中的代码来定位。该模块中的所有类文件可以通过其他模块中的代码来定位。

下面清单包含com.jdojo.resource.test模块的模块声明。本模块中的代码将尝试访问com.jdojo.resource模块中的资源以及本模块中的资源。你需要将com.jdojo.resource模块添加到此模块路径以进行编译。 在 NetBean IDE中com.jdojo.resource.test项目的属性对话框如下图所示。它将com.jdojo.resource模块添加到其模块路径。

Adding module to the module path

// module-info.java
module com.jdojo.resource.test {
    requires com.jdojo.resource;
    exports com.jdojo.resource.test;
}

com.jdojo.resource.test模块中的文件按如下方式排列:

  • module-info.class
  • com\jdojo\resource\test\own.properties
  • com\jdojo\resource\test\ResourceTest.class

该模块包含名为own.properties的资源文件,该文件位于com.jdojo.resource.test包中。 own.properties文件为空。 下面包含ResourceTest类的代码。

// ResourceTest
package com.jdojo.resource.test;
import com.jdojo.exported.AppResource;
import java.io.IOException;
import java.io.InputStream;
public class ResourceTest {
    public static void main(String[] args) {
        // A list of resources
        String[] resources = {
            "java/lang/Object.class",
            "com/jdojo/resource/test/own.properties",
            "com/jdojo/resource/test/ResourceTest.class",
            "unnamed.properties",
            "META-INF/invalid_pkg.properties",
            "com/jdojo/opened/opened.properties",
            "com/jdojo/exported/AppResource.class",
            "com/jdojo/resource/exported.properties",
            "com/jdojo/encapsulated/EncapsulatedTest.class",
            "com/jdojo/encapsulated/encapsulated.properties"
        };
        System.out.println("Using a Module:");
        Module otherModule = AppResource.class.getModule();
        for (String resource : resources) {
            lookupResource(otherModule, resource);
        }
        System.out.println("\nUsing a Class:");
        Class cls = ResourceTest.class;
        for (String resource : resources) {
            // Prepend a / to all resource names to make them absolute names
            lookupResource(cls, "/" + resource);
        }
        System.out.println("\nUsing the System ClassLoader:");
        ClassLoader clSystem = ClassLoader.getSystemClassLoader();
        for (String resource : resources) {
            lookupResource(clSystem, resource);
        }
        System.out.println("\nUsing the Platform ClassLoader:");
        ClassLoader clPlatform = ClassLoader.getPlatformClassLoader();
        for (String resource : resources) {
            lookupResource(clPlatform, resource);
        }
    }
    public static void lookupResource(Module m, String resource) {
        try {
            InputStream in = m.getResourceAsStream(resource);
            print(resource, in);
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void lookupResource(Class cls, String resource) {
        InputStream in = cls.getResourceAsStream(resource);
        print(resource, in);
    }
    public static void lookupResource(ClassLoader cl, String resource) {
        InputStream in = cl.getResourceAsStream(resource);
        print(resource, in);
    }
    private static void print(String resource, InputStream in) {
        if (in != null) {
            System.out.println("Found: " + resource);
        } else {
            System.out.println("Not Found: " + resource);
        }
    }
}

下面是具体的输出:

Using a Module:
Not Found: java/lang/Object.class
Not Found: com/jdojo/resource/test/own.properties
Not Found: com/jdojo/resource/test/ResourceTest.class
Found: unnamed.properties
Found: META-INF/invalid_pkg.properties
Found: com/jdojo/opened/opened.properties
Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties
Using a Class:
Not Found: /java/lang/Object.class
Found: /com/jdojo/resource/test/own.properties
Found: /com/jdojo/resource/test/ResourceTest.class
Not Found: /unnamed.properties
Not Found: /META-INF/invalid_pkg.properties
Not Found: /com/jdojo/opened/opened.properties
Not Found: /com/jdojo/exported/AppResource.class
Not Found: /com/jdojo/resource/exported.properties
Not Found: /com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: /com/jdojo/encapsulated/encapsulated.properties
Using the System ClassLoader:
Found: java/lang/Object.class
Found: com/jdojo/resource/test/own.properties
Found: com/jdojo/resource/test/ResourceTest.class
Found: unnamed.properties
Found: META-INF/invalid_pkg.properties
Found: com/jdojo/opened/opened.properties
Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties
Using the Platform ClassLoader:
Found: java/lang/Object.class
Not Found: com/jdojo/resource/test/own.properties
Not Found: com/jdojo/resource/test/ResourceTest.class
Not Found: unnamed.properties
Not Found: META-INF/invalid_pkg.properties
Not Found: com/jdojo/opened/opened.properties
Not Found: com/jdojo/exported/AppResource.class
Not Found: com/jdojo/resource/exported.properties
Not Found: com/jdojo/encapsulated/EncapsulatedTest.class
Not Found: com/jdojo/encapsulated/encapsulated.properties

lookupResource()方法重载。 它们使用三个类来定位资源:ModuleClassClassLoader。 这些方法将资源名称和资源引用传递给print()方法来打印消息。

main()方法准备了一个资源列表,用来使用不同的资源查找方法查找。 它保存了一个String数组列表:

// A list of resources
String[] resources = {/* List of resources */};

main()方法尝试使用com.jdojo.resource模块的引用查找所有资源。 请注意,AppResource类在com.jdojo.resource模块中,因此AppResource.class.getModule()方法返回com.jdojo.resource模块的引用。

System.out.println("Using a Module:");
Module otherModule = AppResource.class.getModule();
for (String resource : resources) {
    lookupResource(otherModule, resource);
}

该代码找到com.jdojo.resource模块中未命名、无效和打开的包中的所有类文件和资源。 请注意,没有找到java/lang/Object.class,因为它在java.base模块中,而不在com.jdojo.resource模块中。 同样的原因找不到com.jdojo.resource.test模块中的资源。

现在,main()方法使用Resource Test类的Class对象来找到相同的资源,它在com.jojo.resource.test模块中。

Class cls = ResourceTest.class;
for (String resource : resources) {
    // Prepend a / to all resource names to make them absolute names
    lookupResource(cls, "/" + resource);
}

Class对象将仅在com.jdojo.resource.test模块中定位资源,这在输出中是显而易见的。 在代码中,使用斜线预先填写资源名称,因为Class类中的资源查找方法会把资源当作不以斜线开头的相对资源名称来对待,并将该类的包名称添加到该资源名称。

最后,main()方法使用应用程序和平台类加载器来定位同一组资源:

ClassLoader clSystem = ClassLoader.getSystemClassLoader();
for (String resource : resources) {
    lookupResource(clSystem, resource);
}
ClassLoader clPlatform = ClassLoader.getPlatformClassLoader();
for (String resource : resources) {
    lookupResource(clPlatform, resource);
}

类加载器将在类加载器本身或其祖先类加载器已知的所有模块中定位资源。 系统类加载器加载com.jdojo.resource和com.jdojo.resource.test模块,因此它可以根据资源查找规则强制的限制来查找这些模块中的资源。 即引导类加载器从java.base模块加载Object类,因此系统类加载器可以找到java/lang/Object.class文件。

平台类加载器不加载com.jdojo.resource和com.jdojo.resource.test应用程序模块。 在输出中很明显.平台类加载器只发现一个资源,java/lang/Object.class,由父类引导类加载器进行加载。

九. 访问运行时映像中的资源

我们来看几个在运行时映像中访问资源的例子。 在JDK 9之前,可以使用ClassLoader类的getSystemResource()静态方法。 以下是在JDK 8中查找Object.class文件的代码:

import java.net.URL;
...
String resource = "java/lang/Object.class";
URL url = ClassLoader.getSystemResource(resource);
System.out.println(url);
// jar:file:/C:/java8/jre/lib/rt.jar!/java/lang/Object.class

输出显示使用jar方案返回的URL指向rt.jar文件。

JDK 9不再在JAR中存储运行时映像。 它可能在将来更改成内部格式存储。 JDK提供了一种使用jrt方案以与格式和位置无关的方式访问运行时资源的方法。 上面代码在JDK 9中通过使用jrt方案返回一个URL,而不是jar方案:

jrt:/java.base/java/lang/Object.class

Tips
如果你的代码从运行时映像访问资源,并期望使用jar方案的URL,则需要在JDK 9中进行更改,因为在JDK 9中将使用jrt格式获取URL。

使用jrt方案的语法如下:

jrt:/<module-name>/<path>

<module-name>是模块的名称,<path>是模块中特定类或资源文件的路径。 <module-name><path>都是可选的。 jrt:/,指的是保存在当前运行时映像中的所有类和资源文件。 jrt:/<module-name>是指保存在<module-name>模块中的所有类和资源文件。 jrt:/<module-name>/<path>指的是<module-name>模块中名为<path>的特定类或资源文件。 以下是使用jrt方案引用类文件和资源文件的两个URL的示例:

jrt:/java.sql/java/sql/Driver.class
jrt:/java.desktop/sun/print/resources/duplex.png

第一个URL为java.sql模块中java.sql.Driver类的类文件命名。 第二个URL是java.desktop模块中的映像文件sun/print/resources/duplex.png命名。

Tips
可以使用jrt方案访问运行时映像中的资源,但是在使用ModuleClassClassLoader类中的资源查找方式是不可访问的。

可以使用jrt方案创建一个URL。 以下代码片段显示了如何吧一个图片文件读入到Image对象中,以及在运行时映像中把一个类文件读入到字节数组。

// Load the duplex.png into an Image object
URL imageUrl = new URL("jrt:/java.desktop/sun/print/resources/duplex.png");
Image image = ImageIO.read(imageUrl);
// Use the image object here
System.out.println(image);
// Load the contents of the Object.class file
URL classUrl = new URL("jrt:/java.base/java/lang/Object.class");
InputStream input = classUrl.openStream();
byte[] bytes = input.readAllBytes();
System.out.println("Object.class file size: " + bytes.length);

输出结果为:

BufferedImage@3e57cd70: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@67b467e9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859

什么时候可以使用其他形式的jrt方案,以便表示运行时映像中的所有文件和模块中的所有文件? 可以使用jrt方案来引用一个模块来授予Java策略文件的权限。 Java策略文件中的以下条目将为java.activation模块中的代码授予所有权限:

grant codeBase "jrt:/java.activation" {
    permission java.security.AllPermission;
}

许多工具和IDE需要枚举运行时映像中的所有模块,软件包和文件。 JDK 9为了jrt URL方案,附带一个只读NIO FileSystem提供者。 可以使用此提供者列出运行时映像中的所有类和资源文件。 有一些工具和IDE将在JDK 8上运行,但将支持JDK 9的代码开发。这些工具还需要获取JDK 9运行时映像中的类和资源文件列表。 当你安装JDK 9时,它在lib目录中包含一个jrt-fs.jar文件。 可以将此JAR文件添加到在JDK 8上运行的工具的类路径,并使用jrt FileSystem,如下所示。

jrt文件系统包含由斜线(/)表示的根目录,其中包含两个名为包和模块的子目录:

/
/packages
/modules

以下代码片段为jrt URL方案创建了一个NIO FileSystem

// Create a jrt FileSystem
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
The following snippet of code reads an image file and the contents of the Object.class file:
// Load an image from a module
Path imagePath = fs.getPath("modules/java.desktop", "sun/print/resources/duplex.png");
Image image = ImageIO.read(Files.newInputStream(imagePath));
// Use the image object here
System.out.println(image);
// Read the Object.class file contents
Path objectClassPath = fs.getPath("modules/java.base", "java/lang/Object.class");
byte[] bytes = Files.readAllBytes(objectClassPath);
System.out.println("Object.class file size: " + bytes.length);

输出结果为:

BufferedImage@5f3a4b84: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@5204062d transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859

以下代码片段将打印运行时映像中所有模块中的所有类和资源文件。 类似地,可以为包创建·Path`类列举运行时映像中的所有包。

// List all modules in the runtime image
Path modules = fs.getPath("modules");
Files.walk(modules)
     .forEach(System.out::println);

输出结果为:

/modules
/modules/java.base
/modules/java.base/java
/modules/java.base/java/lang
/modules/java.base/java/lang/Object.class
/modules/java.base/java/lang/AbstractMethodError.class
...

我们来看一个从运行时映像访问资源的完整程序。 下面包含名为com.jdojo.resource.jrt的模块的模块声明。

// module-info.java
module com.jdojo.resource.jrt {
    requires java.desktop;
}

接下来是JrtFileSystem类的源代码,它位于com.jdojo.resource.jrt模块中。

// JrtFileSystem.java
package com.jdojo.resource.jrt;
import java.awt.Image;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.imageio.ImageIO;
public class JrtFileSystem {
    public static void main(String[] args) throws IOException {
        // Create a jrt FileSystem
        FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
        // Load an image from a module
        Path imagePath = fs.getPath("modules/java.desktop", "sun/print/resources/duplex.png");
        Image image = ImageIO.read(Files.newInputStream(imagePath));
        // Use the image object here
        System.out.println(image);
        // Read the Object.class file contents
        Path objectClassPath = fs.getPath("modules/java.base", "java/lang/Object.class");
        byte[] bytes = Files.readAllBytes(objectClassPath);
        System.out.println("Object.class file size: " + bytes.length);
        // List 5 packages in the runtime image
        Path packages = fs.getPath("packages");
        Files.walk(packages)
             .limit(5)
             .forEach(System.out::println);
        // List 5 modules’ entries in the runtime image
        Path modules = fs.getPath("modules");
        Files.walk(modules)
             .limit(5)
             .forEach(System.out::println);
    }
}

输出结果为:

BufferedImage@5bfbf16f: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@27d415d9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 41 height = 24 #numDataElements 4 dataOff[0] = 3
Object.class file size: 1859
packages
packages/com
packages/com/java.activation
packages/com/java.base
packages/com/java.corba
modules
modules/java.desktop
modules/java.desktop/sun
modules/java.desktop/sun/print
modules/java.desktop/sun/print/resources

JrtFileSystem类,演示使用jrt URL方案从运行时映像访问资源。
注意,程序仅打包和模块目录中的五个条目。 可以访问java.desktop模块中的sun/print/resources/duplex.png。 java.desktop模块不打开sun.print.resources包。 使用ModuleClassClassLoader类中的任何资源查找方法来定位 sun/print/resources/duplex.png将失败。

十. 使用JDK内部API

JDK由公共API和内部API组成。 公共API旨在用于开发可移植Java应用程序。 JDK中的java.*javax.*org.*包包含在公共API。 如果应用程序仅使用公共API,则可以在支持Java平台的所有操作系统上运行。 这种应用提供的另一个保证是,如果它在JDK版本N中工作,它将继续在JDK版本N + 1中工作。

com.sun.*sun.*jdk.*包用于实现JDK本身,它们组成内部API,这不意味着由开发人员使用。 内部API不能保证在所有操作系统上运行。 com.sun.*sun.*等软件包是Oracle JDK的一部分。 如果使用其他供应商的JDK,这些软件包将不可用。 非Oracle JDK(如IBM的JDK)将使用其他软件包名称来实现其内部API。 下图显示了不同类别的JDK API。

基于其预期用途的JDK API类别

在JDK 9模块化之前,可以使用任何JAR的公共类,即使这些类是JDK内部API。 开发人员和一些广泛使用的库已经使用JDK内部API来方便,或者由于这些API提供的功能难以在JDK之外实现。 这些类的示例是BASE64EncoderBASE64Decoder。 开发人员为了方便使用它们,它们可以作为sun.misc包中的JDK内部API使用,即使它们不难开发。 另一个广泛使用的类是sun.misc包中的Unsafe类。 在JDK之外开发一个类来替代Unsafe类,因为它访问了JDK内部是很困难的。

仅用于方便使用的内部API在JDK之外不被使用,或者它们所存在的支持的替换已经被分类为非关键内部API,并且已经封装在JDK 9中。示例是Sun.misc包中的BASE64EncoderBASE64Decoder类,JDK 8里,Base64.EncoderBase64.Decoder`类作为公共API的一部分添加到java.util包中。

在JDK之外广泛使用但难以开发的内部API被归类为关键的内部API。 如果存在替换,它们被封装在JDK 9中。 封装在JDK 9中但可以使用命令行选项的关键内部API已使用@jdk.Exported注解。 JDK 9不提供以下类的替代,这些类被认为是关键的内部API。 它们可以通过jdk.unsupported模块访问。

com.sun.nio.file.ExtendedCopyOption
com.sun.nio.file.ExtendedOpenOption
com.sun.nio.file.ExtendedWatchEventModifier
com.sun.nio.file.SensitivityWatchEventModifier
sun.misc.Signal
sun.misc.SignalHandler
sun.misc.Unsafe
sun.reflect.Reflection
sun.reflect.ReflectionFactory

Tips
在JDK 9中,大多数JDK内部API已封装在模块中,默认情况下不可访问。但仍然可以使用--add-read非标准命令行选项访问它们。

以下类中的addPropertyChangeListener()removePropertyChangeListener()方法已在JDK 8中弃用,并已从JDK 9中删除:

java.util.logging.LogManager
java.util.jar.Pack200.Packer
java.util.jar.Pack200.Unpacker

可以使用位于JAVA_HOME\bin目录中的jdeps工具来查找代码在JDK内部API上的类级依赖关系。 还需要使用--jdk-internals选项,如下所示:

jdeps --jdk-internals --class-path <class-path> <input-path>

这里,<input-path>可以是类文件,目录或JAR文件的路径。 该命令分析<input-path><class-path>上的所有类。 以下命令打印jersey-common.jar文件中JDK内部API的用法,假设JAR位于C:\Java9Revealed\extlib目录中。

C:\Java9Revealed>jdeps --jdk-internals extlib\jersey-common.jar

下面是部分输出:

jersey-common.jar -> jdk.unsupported
   org.glassfish.jersey.internal.util.collection.ConcurrentHashMapV8 -> sun.misc.Unsafe                                    JDK internal API (jdk.unsupported)
org.glassfish.jersey.internal.util.collection.ConcurrentHashMapV8$TreeBin -> sun.misc.Unsafe                                    JDK internal API (jdk.unsupported)
...

十一. 修补模块内容

有时候,可能需要用另一个版本替换特定模块的类文件和资源进行测试和调试。 在JDK 9之前,可以使用-Xbootclasspath/p选项来实现此目的。 此选项已在JDK 9中删除。在JDK 9中,需要使用--patch-module非标准命令行选项。 此选项可用于javac和java命令。 其语法如下:

--patch-module <module-name>=<path-list>

这里,<module-name>是正在替换其内容的模块的名称。 <path-list>是包含新模块内容的JAR或目录列表; 列表中的每个元素都由特定于主机的路径分隔符分隔,该字符是Windows上的分号和类UNIX平台上的冒号。

可以对同一命令多次使用--patch-module选项,因此可以修补多个模块的内容。 可以修补应用程序模块,库模块和平台模块。

Tips
当使用--patch-module选项时,无法替换module-info.class文件。 试图这样做是默认无视的。

现在,我们将运行一个修补com.jdojo.intro模块的例子。 使用新的Welcome.class文件替换此模块中的Welcome.class文件。 回想一下,我们在第3章中创建了Welcome类。新类将打印一个不同的消息。 新的类声明如下所示。 在源代码中,此类位于com.jdojo.intro.patch 的NetBeans项目中。

// Welcome.java
package com.jdojo.intro;
public class Welcome {
    public static void main(String[] args) {
        System.out.println("Hello Module System.");
        // Print the module name of the Welcome class
        Class<Welcome> cls = Welcome.class;
        Module mod = cls.getModule();
        String moduleName = mod.getName();
        System.out.format("Module Name: %s%n", moduleName);
    }
}

现在,需要使用以下命令为上面新的Welcome类编译源代码:

C:\Java9Revealed>javac -Xmodule:com.jdojo.intro
  --module-path com.jdojo.intro\dist
  -d patches\com.jdojo.intro.patch com.jdojo.intro.patch\src\com\jdojo\intro\Welcome.java

即使删除前两个选项:-Xmodule-module-path,此命令也将成功。 但是,当编译平台类(如java.util.Arrays)时,将需要这些选项。 否则,将收到错误。-Xmodule选项指定要编译的源代码所属的模块名称。 --module-path选项指定在哪里查找-Xmodule选项中指定的模块。 这些选项用于定位编译新类所需的其他类。 在这种情况下,Welcome类不依赖于com.jdojo.intro模块中的任何其他类。 这就是为什么在这种情况下删除这些选项不会影响结果。-d选项指定编译的Welcome.class文件的保存位置。

以下是从com.jdojo.intro模块运行原始Welcome类的命令:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist
--module com.jdojo.intro/com.jdojo.intro.Welcome

输出结果为:

Welcome to the Module System.
Module Name: com.jdojo.intro

现在是使用修补版本运行Welcome类的时候了。 这是执行此操作的命令:

C:\Java9Revealed>java --module-path com.jdojo.intro\dist
  --patch-module com.jdojo.intro=patches\com.jdojo.intro.patch
  --module com.jdojo.intro/com.jdojo.intro.Welcome

输出结果为:

Hello Module System.
Module Name: com.jdojo.intro

当使用--patch-module选项时,在搜索模块路径之前,模块系统会搜索此选项中指定的路径。 请注意,此选项中指定的路径包含模块的内容,但这些路径不是模块路径。

十二. 总结

如果将旧版应用程序迁移到JDK 9,JDK 9进行了一些突破性的更改,这点必须注意。

JDK 9中对JDK的非直观版本控制方案已经进行了改进。JDK版本字符串由以下四个元素组成:版本号,预发布信息,构建信息和附加信息。 只有第一个是强制性的。 正则表达式$vnum(-$pre)?(\+($build)?(-$opt)?)?定义了版本字符串的格式。 一个简短版本的字符串只包含前两个元素:一个版本号,可选的是预发布信息。 可以有一个简短到“9”的版本字符串,其中只包含主版本号。“99.0.1-ea+154-20170130.07.36am”,这个版本字符串包含了所有元素。

JDK 9添加了一个名为Runtime.Version的静态嵌套类,其实例表示JDK版本字符串。 该类没有公共构造函数。 获取其实例的唯一方法是调用其静态方法名parse(String vstr)。 如果版本字符串为空或无效,该方法可能会抛出运行时异常。 该类包含几个方法来获取版本的不同部分。

JDK 9更改了JDK和JRE安装的目录布局。 现在,除了JDK安装包含开发工具和JRE不包含的JMOD格式的平台模块的拷贝之外,JDK和JRE安装之间没有区别。 可以构建自己的JRE(使用jlink工具),它可以包含JRE中需要的JDK的任何部分。

在Java SE 9之前,可以使用“支持的标准覆盖机制”来使用实现“承认标准”或“独立API”的较新版本的类和接口。 这些包括在Java Community Process之外创建的javax.rmi.CORBA包和Java API for XML Processing(JAXP)。 Java SE 9仍然支持这种机制。 在Java SE 9中,需要使用--upgrade-module-path命令行选项。 此选项的值是包含标准标准和独立API的模块的目录列表。

在版本9之前的Java SE允许一个扩展机制,可以通过将JAR放在系统属性java.ext.dirs指定的目录中来扩展运行时映像。 如果未设置此系统属性,则使用jre\lib\ext目录作为其默认值。 Java SE 9不支持扩展机制。 如果需要类似的功能,可以将这些JAR放在类路径的前面。

在版本9之前,JDK使用三个类加载器来加载类。 他们是引导类加载器,扩展类加载器和系统(应用程序)类加载器。 它们分层排列 —— 没有父类的引导类加载器,引导类加载器作为扩展类加载器的父类,并扩展类加载器作为系统类加载器作为的父级。 在尝试加载类型本身之前,类加载器将类型加载要求委托给其父类(如果有)。 JDK 9保持了三类装载机的向后兼容性。 JDK 9不支持扩展机制,所以扩展类加载器没有意义。 JDK 9已经将扩展类加载器重命名为平台类加载器,该引用可以使用ClassLoader类的静态方法getPlatformClassLoader()获取。 在JDK 9中,每个类加载器加载不同类型的模块。

在JDK 9中,默认情况下封装命名模块中的资源。只有当资源处于未命名,无效或打开的包中时,命名模块中的资源才能被另一个模块中的代码访问。名称以.class(所有类文件)结尾的命名模块中的所有资源都可以通过其他模块中的代码访问。可以使用jrt方案的URL来访问运行时映像中的任何资源。

在JDK 9之前,可以使用JDK内部API。 JDK 9中的大多数JDK内部API已被封装。有些通过jdk.unsupported模块来提供。可以使用jdeps工具和--jdk-internals选项来查找代码对JDK内部API的类级依赖性。

有时候,可能需要用另一个版本替换特定模块的类文件和资源进行测试和调试。在JDK 9之前,可以使用已在JDK 9中删除的-Xbootclasspath/p选项来实现。在JDK 9中,需要使用--patch-module非标准命令行选项。 javac和java命令可以使用此选项。

posted @ 2017-07-07 13:10  林本托  阅读(11484)  评论(0编辑  收藏  举报