20230710 9. Java 平台模块系统
模块使类和包可以有选择性地获取,从而使得模块的演化可以受控
多个现有的 Java 模块系统都依赖于类加载器来实现类之间的隔离。但是 Java 9 引入了一个由 Java 编译器和虚拟机支持的新系统,称为 Java 平台模块系统。它被设计用来模块化基于 Java 平台的大型代码基。也可以使用这个系统来模块化我们自己的应用程序。
模块的概念
在面向对象编程中,基础的构建要素就是类。类提供了封装,私有特性只能被具有明确访问权限的代码访问,即,只能被其所属类中的方法访问,这使得对访问情况的推断成为可能。
在Java中,包提供了更高一级的组织方式,包是类的集合。包也提供了一种封装级别,具有包访问权限的所有特性(无论是公有的还是私有的)都只能被同一个包中的方法访问。
但是,在大型系统中,这种级别的访问控制仍显不足。所有公有特性 (即在包的外都也可以访问的特性)可以从任何地方访问。假设我们想要修改或剔除一个很少使用的特性,如果它是公有的,那么就没有办法推断这个变化所产生的影响。
Java 平台的设计者们面对的就是这种情况。过去20年中,JDK 呈跨越式发展,但是有些特性现在明显过时了。例如 CORBA,例如服务端的应用程序不需要 java.awt 包
Java 平合的设计者们在面对规模超大且盘根错节的代码时,认为他们需要一种能够提供更多控制能力的构建机制。因此设计了 Java 平台模块系统,现在成了 Java 语言和虚拟机的一部分。这个系统已经成功地用于 Java API 的模块化,如果愿意,也可以使用这个系统来模块化我们自己的应用程序。
一个 Java 平台模块包含:
- 一个包集合
- 可选地包含资源文件和像本地库这样的其他文件
- 一个有关模块中可访问的包的列表
- 一个有关这个模块依赖的所有其他模块的列表
Java 平台在编译时和在虚拟机中都强制执行封装和依赖。
为什么在我们自己的程序中要考虑使用 Java 平合模块系统而不是传统的使用类路径上的 JAR 文件呢?因为这样做有以下两个优点
- 强封装:我们可以控制哪些包是可访问的,并且无须操心去维护那些我们不想开放给公众去访问的代码。
- 可靠的配置:我们可以避免诸如类重复或丢失这类常见的类路径问题。
对模块命名
模块是包的集合。模块中的包名无须彼此相关。例如,java.sql
模块中就包含 java.sql
、javax.sql
和 javax.transaction.xa
这几个包。并且,正如这个例子所示,模块名和包名相同是完全可行的。
就像路径名一样,模块名是由字母、数字、下划线和句点构成的。而且,和路径名一样,模块之间没有任何层次关系。如果有一个模块是 com.horstmamn
,另一个模块是 com.horstmann.corejava
,那么就模块系统而言,它们是无关的。
当创建供他人使用的模块时,重要的是要确保它的名字是全局唯一的。我们期望大多数的模块名都遵循 “反向域名” 惯例,就像包名一样。
命名模块最简单的方式就是按照模块提供的顶级包来命名。这个惯例可以防止模块中产生包名冲突,因为任何给定的包都只能被放到一个模块中。如果模块名是唯一的,并且包名以模块名开头,那么包名也就是唯一的。
我们可以使用更短的模块名来命名不打算给其他程序员使用的模块
注释:模块名只用于模块声明中。在 Java 类的源文件中,永远都不应该引用模块名,而是应该按照一如既往的方式去使用包名。
模块化的 Hello World 程序
“不具名” 包不能包含在模块中
模块声明位于 module-info.java
的文件中,该文件位于基目录中。按照惯例,基目录的名字与模块名相同。这个文件会以二进制形式编译到包含该模块定义的类文件 module-info.class
中
关键字 module
, requires
, exports
都是限定关键词,只在模块声明中具有特殊含义。
为了让程序作为模块化应用程序来运行,需要指定模块路径,它与类路径相似,但是包含的是模块。还需要以模块名/类名的形式指定主类
java --module-path v2ch09.hellomod --module v2ch09.hellomod/com.horstmann.hello.HelloWorld
# 简写
java -p v2ch09.hellomod -m v2ch09.hellomod/com.horstmann.hello.HelloWorld
对模块的需求
JDK 已经被模块化了,默认需要 java.base
模块
模块不可以循环依赖
模块依赖不具有传递性,不会自动将访问权限传递给其他模块,requires transitive
支持传递依赖
按照 Java 模块系统的用语,模块 M 会在下列情况下读入模块 N
- M 需要 N
- M 需要某个模块,而该模块传递性地需要 N
- N 是 M 或
java.base
模块导出包
通过不导出包可以隐藏这些包
当包被导出时,它的 public
和 protected
的类和接口,以及 public
和 protected
的成员,在模块外部也是可以访问的
但是没有导出的包在其自己的模块之外是不可访问的,这与模块化之前很不相同。
注意:exports
语句后面跟着包名,而 requires
语句后面跟着 模块名
模块没有作用域的概念。不能在不同的模块中放置两个具有相同名字的包。即使是隐藏的包(不导出),也是如此。否则这两个模块无法同时被加载
模块化的 JAR
模块可以通过将其所有的类都置于一个 JAR 文件中而得以部署,其中 module-info.class
在 JAR 文件的根部。这样的 JAR 文件被称为模块的 JAR
如果使用的是像 Maven、Ant 或 Gradle 这样的构建工具,那么只需按照惯用的方式来构建 JAR 文件。只要 module-info.class
包含在内,就可以得到该模块的 JAR文件。
然后,在模块路径中包含该模块化的 JAR,该模块就会被加载。
在创建 JAR 文件时,可以选择指定版本号。使用 --module-version
选项,以及在 JAR 文件名上添加 @
和版本号
Java 平台模块系统并不会使用版本号来解析模块,但是可以通过其他工具和框架来查询版本号
可以通过反射 API 找到版本号
等价于类加载器的模块是一个层。Java 平台模块系统会将 JDK 模块和应用程序模块加载到启动层(boot layer)。程序还可以使用分层 API 加载其他模块。这种程序可以选择考虑模块的版本。Java 期望像 Java EE 应用服务器这样的程序的开发者会利用分层 API 来提供对模块的支持。
模块和反射式访问
如果一个类在某个模块中,对非公有成员的反射式访问将失败
通过使用 opens
关键字,模块可以打开包,从而启动对给定包中的类的所有实例进行反射式访问
module v2ch09.openpkg
{
requires com.horstmann.util;
opens com.horstmann.places;
}
模块也可以声明为 Open (开放的)
open module v2ch09.openpkg
{
requires com.horstmann.util;
}
开放的模块可以授权对其所有包的运行时访问,就像所有的包都用 exports
和 opens
声明过一样。但是,在运行时只有显式导出的包是可访问的。开放模块將模块系统编译时的安全性和经典的授权许可的运行时行为结合在一起。
JAR 文件除了类文件和清单外,还可以包含文件资源,它们可以被 Class.getResourceAsStream
方法加载,现在还可以被 Module.getResourceAsStream
加载。如果资源存储在匹配模块的某个包的目录中,那么这个包必须对调用者是开放的。在其他目录中的资源,以及类文件和清单,可以被任何人读取。
未来的库可能会使用变量句柄而不是反射来读写域。VarHandle
类似于 Field
自动模块
Java 平台模块系统提供了两种机制来填补将当今的前模块化世界与完全模块化应用程序割裂开来的鸿沟:自动化模块和不具名模块
如果是为了迁移,我们可以通过把任何 JAR 文件置于模块路径的目录而不是类路径的目录中,实现将其转换成一个模块。模块路径上没有 module-info.class
文件的 JAR 被称为自动模块。
自动模块具有下面的特性:
- 模块隐式地包含对其他所有模块的
requires
子句。 - 其所有包都被
exports
导出,且是开放的。 - 如果在 JAR 文件清单
META-INF/MANIFEST.MF
中具有键为Automatic-Module-Name
的项,那么它的值会变为模块名。 - 否则,模块名将从 JAR 文件名中获得,将文件名中尾部的版本号删除,并将非字母数字的字符替换为句点。
前两条规则表明自动模块中的包的行为和在类路径上一样。使用模块路径的原因是为了让其他模块受益,使得它们可以表示对这个模块的依赖关系。
例如,假设我们正在实现一个处理 CSV 文件的模块,并使用了 Apache Commons CSV 库。我们想要在 mooule-info.java
文件中表示该模块需要依赖 Apache Commons CSV。如果在模块路径中添加 commons-csv-1.9.0.jar
,那么我们的模块就可以引用这个模块了。它的名字是 commons.csv
,因为去掉了尾部版本号-1.9.0,而非字母数字字符 -
被替换成了句点。
这个名字也许算是一个可接受的模块名,因为 Commons CSV 人们耳熟能详,其他人也不太可能会用这个名字来命名其他的模块。但是,如果这个JAR 文件的维护者同意保留反向域名,使用更好的顶级包名 org.apache.commons.csv
作为模块名,那会显得更好。他们只需在 JAR 中的 META-INF/MANIFEST.MF
文件里添加一行:
Automatic-Module-Name: org.apache.commons.csv
最终,我们期望他们能够在 module-info.java
中添加保留的模块名将这个 JAR 文件转换成一个真正的模块,而每个用该模块名引用了这个 CSV 模块的模块也都能够继续工作。
注释:模块的迁移计划是一项伟大的社会实验,没有人知道它是否能够顺利实施。在将第三方的 JAR 放到模块路径文前,请检查它们是否是模块化的。如果不是,那他们的清单是否有模块名;如果没有,仍旧需要将这样的 JAR 转换成自动模块,但是要准备好以后更新该模块名。
不具名模块
任何不在模块路径中的类都是不具名模块的一部分。从技术上说,可能会有多个不具名模块,但是它们合起来看就像是单个不具名的模块。与自动模块一样,不具名模块可以访问所有其他的模块,它的所有包都会被导出,并且都是开放的。
但是,没有任何明确模块可以访问不具名的模块。(明确模块是指既不是自动模块也不是不具名模块的模块,即,mooule-info.class
在模块路径上的模块)换句话说,明确模块总是可以避免“类路径的坑”。
例如,考虑前一节的程序,假设将 commons-csv.1.9.0.jar
放到类路径而不是模块路径上:
java --module-path v2ch09.automod \
--class-path commons-csv-1.9.0.jar \
-m v2ch09.automod/com.horstmann.places.CSVDemo
现在,这个程序将无法启动:
Error occurred during initialization of boot layer
java.lang.module.FindException: Module commons.csv not found, required by vache.automod
因此,迁移到 Java 平台模块系统必须按照自底向上的方式处理:
- Java 平台自身被模块化。
- 接下来,库被模块化,要么通过使用自动模块,要么将它们转换为明确模块。
- 一旦应用程序使用的所有库都被模块化,就可以将应用程序的代码转换为一个模块。
注释:自动模块可以读取不具名模块,因此它们的依赖关系放在类路径中。
创建普通 Maven 项目时,目录结构中没有 module-info.java ,默认就是不具名模块,所以可以访问所有其他模块
用于迁移的命令行标识
--illegal-access
--add-exports
--add-opens
传递的需求和静态的需求
requires
的两种变体
requires transitive
声明了传递依赖
requires transitive
的另一种用法是聚集模块,即没有任何包,只有传递性需求的模块。java.se
就是这样的模块
module java.se {
requires transitive java.compiler;
requires transitive java.datatransfer;
requires transitive java.desktop;
requires transitive java.instrument;
requires transitive java.logging;
requires transitive java.management;
requires transitive java.management.rmi;
requires transitive java.naming;
requires transitive java.net.http;
requires transitive java.prefs;
requires transitive java.rmi;
requires transitive java.scripting;
requires transitive java.security.jgss;
requires transitive java.security.sasl;
requires transitive java.sql;
requires transitive java.sql.rowset;
requires transitive java.transaction.xa;
requires transitive java.xml;
requires transitive java.xml.crypto;
}
requires static
声明一个模块必须在编译时出现,而在运行时是可选的。两个用例:
- 访问在编译时进行处理的注解,而该注解是在不同的模块中声明的
- 对于位于不同模块中的类,如果它可用,就使用它,否则就执行其他操作
try
{
new oracle.jdbc.driver.OracleDriver();
}
catch (NoClassDefFoundError er)
{
// ...
}
限定导出和开放
java.base
模块包含下面的语句:
exports sun.net to
java.net.http,
jdk.naming.dns;
这样的语句被称为限定导出,所列的模块可以访问这个包,但是其他模块不行。
过多地使用限定导出表明模块化结构比较糟糕。尽管如此,在模块化现有代码基时,这种情况还是会发生。这里,sun.net
包被置于 java.base
模块中,因为该模块最需要使用这个包。但是,有几个其他的模块也会用到这个包。Java 平合的设计者们并不希望让 java.base
包变得更大,并且不希望内部的 sun.net
包变成对所有代码都普遍可用。在新创建的项目中,人们可以设计更模块化的 API
类似地,可以将 opens
语句限制到具体的模块。
服务加载
ServiceLoader
类提供了一种轻量级机制,用于将服务接口与实现匹配起来。Java 平台模块系统使得这种机制更易于使用。
过去,服务接口的实现是通过将文本文件放置在包含实现类的 JAR 文件的 META-INF/services
目录中而提供给服务消费者的。模块系统提供了一种更好的方式,与提供文本文件不同,可以添加语句到模块描述符中
java.base
中声明提供服务实现
provides java.util.random.RandomGenerator with
java.security.SecureRandom,
java.util.Random,
java.util.SplittableRandom;
这与 META-INF/services
的方式等价
同样在 java.base
模块声明中,描述使用:
uses java.util.random.RandomGenerator;
provides
和 uses
声明的效果,是使消费该服务的模块允许访问私有实现类
操作模块的工具
- jdeps 工具可以分析给定的 JAR 文件集之间的依赖关系
- jlink 工具可以产生执行时无需单独的 Java 运行时环境的应用程序。所产生的镜像比整个 JDK 要小得多。jlink 的关键是它将运行应用程序所需的最小的模块集打包在一起
- jimage 命令审视运行时镜像
- jmod 工具可以构建并审视包含在 JDK 中的模块文件
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
2020-01-13 20200113 SpringBoot整合MyBatis