文 by / 林本托
Tips
做一个终身学习的人。
在此章节中,主要介绍以下内容:
在JDK 9之前Java源代码用于编写,打包和部署的方式以及该方法的潜在问题
JDK 9中有哪些模块
如何声明模块及其依赖关系
如何封装模块
什么是模块路径
什么是可观察的模块
如何打印可观察模块的列表
如何打印模块的描述
本章旨在为你简要概述JDK 9中引入的模块系统。后续章节将详细介绍所有这些概念,并附有实例。 不要担心,如果你第一次不了解所有模块相关的概念。 一旦你获得开发模块代码的经验,你可以回来并重新阅读本章。
一. Java 9 之前的开发
在 JDK 9之前,开发一个 Java 应用程序通常包括以下步骤:
Java源代码以Java类型(如类,接口,枚举和注释)的形式编写。
不同的Java类型被安排在一个包(package)中,而且始终属于一个明确或默认的包。 一个包是一个逻辑的类型集合,本质上为它包含的类型提供一个命名空间。 即使声明为public,包可能包含公共类型,私有类型和一些内部实现类型。
编译的代码被打包成一个或多个JAR文件,也称为应用程序JAR,因为它们包含应用程序代码。 一个程序包中的代码可能会引用多个JAR。
应用程序可能使用类库。 类库作为一个或多个JAR文件提供给应用程序使用。
通过将所有JAR文件,应用程序JAR文件和JAR类库放在类路径上来部署应用程序。
下图显示了JAR文件中打包的代码的典型布局。 该图仅显示了包和Java 类型,不包括其他内容,如manifest.mf文件和资源文件。
20多年来,Java社区以这种编写,编译,打包和部署Java代码的方式开发。 但是,20年漫长的旅程并没有像你所希望的一样顺利! 这样安排和运行Java代码就存在固有的问题:
一个包只是一个类型的容器,而不强制执行任何可访问性边界。包中的公共类型可以在所有其他包中访问;没有办法阻止在一个包中公开类型的全局可见性。
除了以java和javax开头的包外,包应该是开放扩展的。如果你在具有包级别访问的JAR中进行了类型化,则可以在其他JAR中访问定义与你的名称相同的包中的类型。
Java运行时会看到从JAR列表加载的一组包。没有办法知道是否在不同的JAR中有多个相同类型的副本。Java运行时首先加载在类路径中遇到的JAR中找到的类型。
Java运行时可能会出现由于应用程序在类路径中需要的其中一个JAR引起的运行时缺少类型的情况。当代码尝试使用它们时,缺少的类型会引起运行时错误。
在启动时没有办法知道应用程序中使用的某些类型已经丢失。还可以包含错误的JAR文件版本,并在运行时产生错误。
这些问题在Java社区中非常频繁和臭名昭着,他们得到了一个名字 ——JAR-hell。
包装JDK和JRE也是一个问题。 它们作为一个整体作为使用,从而增加了下载时间,启动时间和内存占用。 单体JRE使得Java不可能在内存很小的设备上使用。 如果将Java应用程序部署到云端,则需要支付更多的费用购买更多的使用内存。 大多数情况下,单体JRE使用的内存比所需的内存多,这意味着需要为云服务支付更多的内存。 Java 8中引入的Compact配置文件通过允许将JRE的一个子集打包在称为紧凑配置文件的自定义运行时映像中,大大减少了JRE大小,从而减少了运行时内存占用。
Tips
在早期访问版本中,JDK 9包含三个名为java.compact1,java.compact2和java.compact3的模块,这些模块对应于JDK 8中的三个compact配置文件。之后,它们被删除,因为JDK中的模块可以完全控制在自定义JRE中包含的模块列表。
可以将JDK 9之前的JDK/JRE中的这些问题分为三类:
不可靠的配置
弱封装
JDK/JRE的单体结构
下图显示了Java运行时如何看到类路径上的所有JAR,以及如何从其他JAR访问一个JAR中的代码,没有任何限制,除了在访问控制方面由类型声明指定的代码。
Java 9通过引入开发,打包和部署Java应用程序的新方法来解决这些问题。 在Java 9中,Java应用程序由称为模块的小型交互组件组成。 Java 9也已经将JDK/JRE组织为一组模块。
二. 全新的模块系统
Java 9引入了一个称为模块的新的程序组件。 您可以将Java应用程序视为具有明确定义的边界和这些模块之间依赖关系的交互模块的集合。 模块系统的开发具有以下目标:
可靠的配置
强封装
模块化JDK/JRE
这些目标是解决Java 9之前开发和部署Java应用程序所面临的问题。
可靠的配置解决了用于查找类型的容易出错的类路径机制的问题。 模块必须声明对其他模块的显式依赖。 模块系统验证应用程序开发的所有阶段的依赖关系 —— 编译时,链接时和运行时。 假设一个模块声明对另一个模块的依赖,并且第二个模块在启动时丢失。 JVM检测到依赖关系丢失,并在启动时失败。 在Java 9之前,当使用缺少的类型时,这样的应用程序会生成运行时错误(不是在启动时)。
强大的封装解决了类路径上跨JAR的公共类型的可访问性问题。 模块必须明确声明其中哪些公共类型可以被其他模块访问。 除非这些模块明确地使其公共类型可访问,否则模块不能访问另一个模块中的公共类型。 Java 9中的公共类型并不意味着程序的所有部分都可以访问它。 模块系统增加了更精细的可访问性控制。
Tips
Java 9通过允许模块在开发的所有阶段声明明确的依赖关系并验证这些依赖关系来提供可靠的配置。它通过允许模块声明其公共类型可以访问其他模块的软件包来提供强大的封装。
JDK 9通过将其前身的体结构分解成一组称为平台模块的模块来重写。 JDK 9还引入了一个可选的阶段,称为链接时,这可能在编译时和运行时之间发生。 在链接期间,使用一个链接器,它是JDK 9附带的一个名为jlink的工具,用于创建应用程序的自定义运行时映像,其中仅包含应用程序中使用的模块。 这将运行时的大小调整到最佳大小。
三. 什么是模块化
模块是代码和数据集合。 它可以包含Java代码和本地代码。 Java代码被组织为一组包含诸如类,接口,枚举和注解等类型的类。 数据可以包括诸如图像文件和配置文件的资源。
对于Java代码,模块可以看做零个或多个包的集合。 下图显示了三个名为policy,claim和utility的模块,其中policy模块包含两个包,claim模块包含一个包,而utility模块不包含任何包。
一个模块不仅仅是一个包的容器。 除了其名称,模块定义包含以下内容:
所需的其他模块(或依赖于)的列表
导出的软件包列表(其公共API),其他模块可以使用
开放的软件包(其整个API,公共和私有)到其他反射访问模块的列表
使用的服务列表(或使用java.util.ServiceLoader类发现和加载)
提供的服务的实现列表
在使用这些模块时,可以使用这些方面中的一个或多个。
Java SE 9平台规范将平台划分为称为平台模块的一组模块。 Java SE 9平台的实现可能包含一些或所有平台模块,从而提供可扩展的Java运行时。 标准模块的名字是以Java 为前缀。 Java SE标准模块的示例有java.base,java.sql,java.xml和java.logging。 支持标准平台模块中的API,供开发人员使用。
非标准平台模块是JDK的一部分,但未在Java SE平台规范中指定。 这些JDK特定的模块的名称以jdk为前缀。 JDK特定模块的示例是jdk.charsets,jdk.compiler,jdk.jlink,jdk.policytool和jdk.zipfs。 JDK特定模块中的API不适用于开发人员。 这些API通常用于JDK本身以及不能轻易获得使用Java SE API所需功能的库开发人员使用。 如果使用这些模块中的API,则可能会在未经通知的情况下对其进行支持或更改。
JavaFX不是Java SE 9平台规范的一部分。 但是,在安装JDK/JRE时,会安装与JavaFX相关的模块。 JavaFX模块名称以javafx为前缀。 JavaFX模块的示例是javafx.base,javafx.controls,javafx.fxml,javafx.graphics和javafx.web。
作为Java SE 9平台的一部分的java.base模块是原始模块。 它不依赖于任何其他模块。 模块系统只知道java.base模块。 它通过模块中指定的依赖关系发现所有其他模块。 java.base模块导出核心Java SE软件包,如java.lang,java.io,java.math,java.text,java.time,java.util等。
四. 模块依赖关系
包括JDK 8之前的版本,一个包中的公共类型可以被其他包访问,没有任何限制。 换句话说,包没有控制它们包含的类型的可访问性。 JDK 9中的模块系统对类型的可访问性提供了细粒度的控制。
模块之间的可访问性是所使用的模块和使用模块之间的双向协议:模块明确地使其公共类型可供其他模块使用,并且使用这些公共类型的模块明确声明对第一个模块的依赖。 模块中的所有未导出的软件包都是模块的私有的,它们不能在模块之外使用。
将包中的 API 设置为公共供其他模块使用被称之为导出包。如果名为policy的模块将名为pkg1的包设置为公共类型可用于其他模块访问,则说明policy模块导出包pkg1。如果名为claim的模块声明对policy模块的依赖性,则称之为claim模块读取(read)policy模块。这意味着,在claim模块内部可以访问p