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

Tips
作一个终身学习的人。html

Java 9

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

  • 新的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版本控制方案,所以人们很容易理解,易于程序解析,并遵循行业标准版本控制方案。git

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

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

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

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

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

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

$vnum(-$pre)?

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

1. 版本号

版本号是按句点分隔的元素序列。 它能够是任意长度。 其格式以下:数据库

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

版本号能够由一到四个元素组成,以下所示:bootstrap

$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命令可使用此选项。

相关文章
相关标签/搜索