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。

推荐阅读更多精彩内容