模块系统 JPMS

Java模块系统(Java Platform Module System,JPMS)是Java 9引入的模块化特性,起源于"项目 Jigsaw(拼图)",该项目的主要目的如下:

简化大型应用与库的构建与维护

模块化系统通过module-info.java显式声明模块依赖关系,取代传统的类路径(classpath)管理,解决了依赖冲突和复杂性问题。例如,开发者可通过requires关键字定义模块依赖,通过exports控制包可见性,使代码结构更清晰。

提升Java SE及JDK的安全性与可维护性

对于安全性来说,模块内部的包默认不可被外部访问,仅通过显式导出的接口暴露功能,减少了恶意代码攻击的风险;对于维护性,模块依赖在编译期和运行期均被严格校验,避免类路径下常见的"缺失依赖"问题。

优化应用性能

模块化系统允许JVM按需加载模块,减少内存占用并提升启动速度。例如,通过模块化后的JDK 9,开发者可裁剪不需要的模块,生成更轻量的运行时环境。

适应小型设备与云部署场景

模块化后的 JDK 被拆分为95个独立模块(如java.base为核心模块),开发者可仅包含所需模块,实现"按需定制",尤其适合资源受限的嵌入式设备和云原生应用。

总而言之,Project Jigsaw 通过模块化系统(JPMS)解决了Java长期存在的依赖管理、安全性和性能问题,同时为现代应用场景(如物联网、云原生)提供了灵活的基础设施。其核心价值体现在强封装、依赖透明化运行时轻量化三个方面。

那到底什么是Java模块系统(JPMS)呢?看一下官网的介绍:

The Java Platform module system introduces a new kind of Java programing component, the module, which is a named, self-describing collection of code and data.

模块(Module)是一种命名且自描述的代码与数据集合 ,也就是说模块包括两部分:命名与自描述代码与数据。简单来说,模块是在 Package 包上再次做了一次功能的划分,一个新的抽象级别,"Package of Java Packages",包的集合。

Class 是字段和方法的集合,Package 是 Class 的集合,而 Module 是 Package 的集合。

有人说,JDK 9 模块 能解决 "JAR 地狱" 问题。JAR Hell 问题主要表现为:

类冲突

多个 JAR 包中包含同名全限定类(如 com.example.Util),JVM 按类路径顺序加载其中一个,可能导致逻辑错误。

版本冲突

同一库的不同版本共存于类路径,JVM 只能按顺序随机加载其中一个版本,如果项目依赖库 A(需 v2.0)和库 B(需 v1.0),但库 B 自身又依赖库 A v1.0,那JVM加载哪个版本的库 A 都不对,可能导致导致 NoSuchMethodErrorNoClassDefFoundError 等错误。

隐式依赖泄漏

类路径中所有 JAR 的类对应用全局可见,即使某些类本应被封装(如内部工具类),也可能被意外访问或覆盖。

JAR Hell 本质就是JAR文件,是个"哑容器",缺乏元数据支持,不会说依赖谁,开放谁,仅仅是代码的集合,那 JDK 9 模块 到底是啥呢?下面从JDK默认的模块入手。

JDK 默认模块

由上图可知,JDK 原有的 jar 包已经被一个个功能单一的模块所替代。下面以 java.base 这个Java 平台的根基模块为例,简单分析一个模块长什么样。

由图可知,java.base 包含了 java.langjava.utiljava.iojava.net 等基础包,作为 Package 的集合,这没问题,要有 代码与数据 嘛。

除此之外,java.base 还包含了一个 module-info.class 文件,这个就是所谓的命名与自描述,也就是模块的描述文件。节选如下:

java 复制代码
/**
 * Defines the foundational APIs of the Java SE Platform.
 * ...
 * @since 9
 */
module java.base { // 模块名

    /** 模块导出的包  **/
    exports java.io;
    exports java.lang;
    exports java.lang.annotation;
    // ... 还有众多其他包的导出,这里省略部分

    /** 有条件导出的包  **/
    exports com.sun.crypto.provider to jdk.crypto.cryptoki;
    exports com.sun.security.ntlm to java.security.sasl;
    // ...  还有众多其他有条件导出的包,这里省略部分

    /** 模块使用的服务  **/
    uses java.lang.System.LoggerFinder;
    uses java.net.ContentHandlerFactory;


    /** 模块提供的服务  **/
    provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider;
    provides java.util.random.RandomGenerator with
        java.security.SecureRandom,
        java.util.Random,
        java.util.SplittableRandom;


}

可以看到,module-info 这个描述模块的 java文件使用的语法,和普通的类 java文件使用的语法一点也不一样。

不着急了解语法,先稍微过一下JDK拆分出来的模块有哪些,都是干什么的,今后项目按需引用对应的模块即可。

模块 功能
java.base 是 Java 平台的根基模块,所有其他模块都直接或间接地依赖它。包含了 java.langjava.utiljava.iojava.net 等基础包,提供了字符串处理、集合框架、输入输出、网络通信等核心功能。
java.compiler 提供 Java 编程语言的编译支持,包含了用于编译 Java 代码的工具和 API,像注解处理和编译过程中的代码分析等。
java.datatransfer 支持在不同应用程序和组件之间进行数据传输,主要用于实现剪贴板操作和拖放功能。
java.desktop 包含构建桌面应用程序所需的类,例如 AWT(抽象窗口工具包)和 Swing 相关的类,用于创建图形用户界面(GUI)。
java.instrument 允许 Java 代理程序在运行时修改类文件,常用于实现代码注入、性能监控等功能。
java.logging 提供 Java 标准的日志记录框架,包含 java.util.logging 包中的类和接口,可对日志进行灵活配置和管理。
java.management 提供 Java 管理扩展(JMX)的支持,用于管理和监控 Java 应用程序、系统资源等。
java.management.rmi 支持通过 RMI(远程方法调用)进行 JMX 管理,使得可以在远程机器上对 Java 应用程序进行管理和监控。
java.naming 提供 Java 命名和目录接口(JNDI),用于在分布式系统中查找和访问资源,如数据库、文件系统等。
java.net.http 提供了用于 HTTP 客户端和服务器编程的 API,支持 HTTP/1.1 和 HTTP/2 协议,方便进行网络通信。
java.prefs 提供了一种跨平台的方式来存储和检索用户和系统的偏好设置,数据可以持久化保存。
java.rmi 支持 Java 远程方法调用,允许在不同的 Java 虚拟机(JVM)之间进行方法调用,实现分布式计算。
java.scripting 提供了 Java 脚本引擎的 API,允许在 Java 程序中嵌入和执行脚本语言,如 JavaScript、Groovy 等。
java.security.jgss 提供了通用安全服务应用程序接口(GSS-API)的 Java 实现,用于实现安全认证和数据加密。
java.security.sasl 提供了简单认证和安全层(SASL)的 Java 实现,用于在网络通信中进行身份验证和数据安全传输。
java.smartcardio 提供了与智能卡进行交互的 API,支持对智能卡的读写操作,常用于安全认证和支付等场景。
java.sql 提供了 Java 数据库连接(JDBC)的 API,用于与关系型数据库进行交互,执行 SQL 查询、更新等操作。
java.sql.rowset 是 JDBC 的扩展,提供了离线的、可滚动的、可更新的结果集,方便在不同环境下处理数据库数据。
java.transaction.xa 提供了对分布式事务的支持,允许在多个资源管理器(如数据库、消息队列)之间进行事务处理。
java.xml 提供了 Java 对 XML 处理的支持,包含了用于解析、生成和操作 XML 文档的类和接口。
java.xml.crypto 提供了 XML 加密和签名的 API,用于保证 XML 数据的安全性和完整性。
jdk.accessibility 提供了对 Java 应用程序的辅助功能支持,使得残障人士可以更方便地使用 Java 应用程序。
jdk.attach 允许在运行时附加到其他 Java 虚拟机(JVM)上,常用于实现调试、监控等工具。
jdk.charsets 提供了额外的字符集支持,扩展了 Java 对不同字符编码的处理能力。
jdk.compiler 包含了 Java 编译器(javac)的实现,是 Java 开发工具包(JDK)的核心组件之一。
jdk.crypto.cryptoki 提供了对 PKCS #11 标准的支持,用于与硬件安全模块(HSM)等加密设备进行交互。
jdk.crypto.ec 提供了椭圆曲线加密(ECC)算法的支持,增强了 Java 的加密能力。
jdk.crypto.mscapi 提供了对 Microsoft CryptoAPI 的支持,用于在 Windows 系统上进行加密操作。
jdk.dynalink 提供了动态链接的支持,允许在运行时动态地绑定方法调用,常用于实现动态语言的运行时环境。
jdk.editpad 可能与文本编辑相关的工具模块,用于提供简单的文本编辑功能。
jdk.graal.compiler Graal 编译器是一个高性能的即时编译器(JIT),该模块提供了 Graal 编译器的核心功能。
jdk.graal.compiler.management 提供了对 Graal 编译器的管理和监控功能,允许开发者查看和调整编译器的参数。
jdk.hotspot.agent 提供了与 HotSpot 虚拟机进行交互的代理,用于实现对 HotSpot 虚拟机的监控和管理。
jdk.httpserver 提供了一个简单的 HTTP 服务器实现,可用于快速搭建 HTTP 服务。
jdk.incubator.vector 包含了向量 API 的实验性实现,用于支持高效的向量计算,提高数据处理性能。
jdk.internal.ed 包含了 JDK 内部使用的一些工具和类,通常不建议开发者直接使用。
jdk.internal.jvmstat 提供了对 JVM 状态信息的统计和监控功能,可用于性能分析和调优。
jdk.internal.le JDK 内部使用的一些低级功能模块,用于实现一些底层的系统交互。
jdk.internal.opt 可能包含了一些 JDK 内部的优化相关的功能和类。
jdk.internal.vm.ci 提供了虚拟机编译器接口(VMCI),用于与不同的编译器进行交互。
jdk.jartool 包含了处理 JAR 文件的工具和类,例如创建、解压、查看 JAR 文件等操作。
jdk.javadoc 提供了生成 Java 文档的工具和 API,用于根据 Java 源代码生成 HTML 格式的文档。
jdk.jcmd 提供了一个命令行工具,用于向正在运行的 JVM 发送诊断命令,进行性能分析和故障排查。
jdk.jconsole 是一个可视化的监控工具,用于监控和管理 Java 应用程序的性能和资源使用情况。
jdk.jdeps 用于分析 Java 类文件、JAR 文件或模块的依赖关系,帮助开发者了解项目的依赖结构。
jdk.jdi 提供了 Java 调试接口(JDI),允许开发者实现自定义的调试工具。
jdk.jdwp.agent 是 Java 调试线协议(JDWP)的代理,用于在调试器和被调试的 JVM 之间进行通信。
jdk.jfr Java 飞行记录器(JFR),用于收集和分析 Java 应用程序的运行时数据,帮助开发者进行性能调优和故障排查。
jdk.jlink 是 JDK 9 引入的一个工具,用于创建自定义的 Java 运行时映像,只包含应用程序所需的模块。
jdk.jpackage 用于将 Java 应用程序打包成可执行的安装包,支持不同操作系统的安装格式。
jdk.jshell 是一个交互式的 Java 编程环境,允许开发者在命令行中直接输入和执行 Java 代码。
jdk.jsobject 提供了在 Java 中与 JavaScript 对象进行交互的支持,常用于 Java 和 JavaScript 混合编程的场景。
jdk.jstatd 是一个守护进程,用于收集和提供 JVM 的统计信息,配合其他工具进行性能监控。
jdk.localedata 包含了 Java 支持的各种语言和地区的本地化数据,如日期、时间、货币等的格式化规则。
jdk.management 提供了对 Java 应用程序和 JVM 的管理和监控功能,包含了一些管理接口和工具。
jdk.management.agent 是 JDK 管理代理,用于启动和管理 JMX 服务,允许外部工具对 Java 应用程序进行管理。
jdk.management.jfr 提供了对 Java 飞行记录器(JFR)的管理和监控功能,允许开发者控制 JFR 的记录和分析过程。
jdk.naming.dns 提供了通过 DNS 进行命名服务的支持,允许在 Java 程序中使用 DNS 查找资源。
jdk.naming.rmi 支持通过 RMI 进行命名服务,结合了 RMI 和 JNDI 的功能。
jdk.net 提供了一些网络编程的辅助类和工具,扩展了 Java 的网络编程能力。
jdk.nio.mapmode 提供了对内存映射文件的额外支持,允许高效地访问大文件。
jdk.random 提供了更强大的随机数生成器,支持不同的随机数算法和种子管理。
jdk.sctp 提供了对流控制传输协议(SCTP)的支持,用于实现可靠的网络通信。
jdk.security.auth 提供了 Java 安全认证的相关类和接口,用于实现用户身份验证和授权。
jdk.security.jgss java.security.jgss 类似,提供了通用安全服务应用程序接口(GSS-API)的 JDK 实现。
jdk.unsupported 包含了一些不推荐使用或不被官方支持的 API,通常用于兼容旧代码。
jdk.unsupported.desktop 包含了一些不推荐使用的桌面相关的 API,用于兼容旧的桌面应用程序。
jdk.xml.dom 提供了对 DOM(文档对象模型)的支持,用于解析和操作 XML 文档。
jdk.zipfs 提供了对 ZIP 文件系统的支持,允许将 ZIP 文件作为文件系统进行操作。

JDK9 模块语法

module-info.java 作为模块的描述文件,包含以下几部分:

  • Name -- 模块的名称
  • Dependencies -- 此模块所依赖的其他模块的列表
  • Public Packages -- 希望从模块外部访问的所有包的列表
  • Services Offered -- 可供其他模块使用的服务实现
  • Services Consumed -- 允许当前模块成为服务的使用者
  • Reflection Permissions -- 明确允许其他类使用反射来访问包的私有成员

基本结构如下:

java 复制代码
module <模块名> {
    // 模块指令(requires, exports, opens 等)
}

模块名一般使用反向域名格式(如 com.cango.core),全小写,禁止使用保留关键字。

模块依赖 requires

java 复制代码
requires {RequiresModifier} ModuleName ;

使用 requires 关键字来声明当前模块依赖的其他模块。RequiresModifier 不同,表示依赖的方式不同。

普通依赖:表明当前模块在编译和运行时都依赖指定的模块

java 复制代码
module com.example.myapp { 
    requires java.sql; // 依赖 java.sql 模块 
}

静态依赖:表示该依赖仅在编译时需要,运行时不是必需的

java 复制代码
module com.example.app { 
    requires static com.example.debugutils; 
}

传递依赖:当前模块的调用者自动依赖此依赖模块

java 复制代码
module com.example.library { 
    requires transitive com.example.util; 
}

如上,com.example.library 模块使用 requires transitive 声明了对 com.example.util 模块的传递依赖。这样一来,若存在其他模块依赖 com.example.library 模块,就会自动依赖 com.example.util 模块。

对于 Maven 来说,依赖的传递的自动完成的------当一个项目依赖于另一个项目时,它也会自动获取另一个项目所依赖的项目。而 JDK9 的模块必须显式的使用关键字 transitive 来传递依赖,所以JDK9的模块更注重代码的封装性

导出包 exports

java 复制代码
exports PackageName [to ModuleName {, ModuleName}] ; 

使用 exports 关键字来指定当前模块对外暴露的包,其他模块可以访问这些包中的公共类和接口。

完全导出:将包导出给所有依赖该模块的模块

java 复制代码
module com.example.myapp { 
    exports com.example.myapp.api; 
}

有条件导出:只将包导出给指定的模块

java 复制代码
module com.example.myapp { 
    exports com.example.myapp.internal to com.example.anotherapp; 
}

默认情况下,模块不会向其他模块公开其任何 API。这种强大的封装是最初创建模块的动机之一。

开放包 opens

java 复制代码
opens PackageName [to ModuleName {, ModuleName}] ;

opens 关键字用于允许其他模块在运行时通过反射访问指定包中的类和成员。

完全开放:允许所有模块进行反射访问

java 复制代码
module com.example.myapp { 
    opens com.example.myapp.reflection; 
}

有条件开放:只允许指定的模块进行反射访问

java 复制代码
module com.example.myapp { 
    opens com.example.myapp.internal.reflection to com.example.testmodule; 
}

在 Java 9 之前,可以使用反射访问包中的任意类型和成员,甚至是私有 的,由于 JDK 9 的模块强制实施强封装 ,所以需要 opens 指令显式授予其他模块反射的权限,当然仅限反射权限。

服务使用与提供 uses 和 provides

js 复制代码
uses TypeName ;
provides TypeName with TypeName {, TypeName} ;

使用 uses 关键字声明当前模块使用的服务接口。

java 复制代码
module com.example.app { 
    uses com.example.service.MyService; 
}

使用 provides 关键字声明当前模块提供的服务接口及其实现类。

java 复制代码
module com.example.serviceprovider { 
    provides com.example.service.MyService with com.example.service.impl.MyServiceImpl; 
}

自定义模块

随便搭建一个Maven多模块项目,当前的模块仅仅是项目的模块,并不是JDK9的模块,现在来改造一下。

  • module-demo-api:API接口模块,提供对外访问的接口。
  • module-demo-common:公共模块,提供一些公用工具。
  • module-demo-mapper:数据库交互模块。
  • module-demo-service:服务模块。
  • module-demo-pojo:实体类模块。

当然,实际项目可能不会这么拆分,这里为了演示。

module-demo-common 作为公共模块,谁都可以用,那就全部开放:

module-demo-service作为服务模块,对外提供接口就好啦。

意思就是这个意思,没啥难理解的。

模块类型

模块系统中有四种类型的模块:System ModulesApplication ModulesAutomatic ModulesUnnamed Module

系统模块(System Modules)

就是 JDK 和 JRE 的官方提供的模块,如 java.base(基础核心库)、java.sql(数据库连接)、...。

应用模块(Application Modules)

我们开发者自己自定义的业务模块,需编写 module-info.java 文件进行模块描述。

自动模块(Automatic Modules)

非模块化 JAR 文件置于模块路径 时自动生成的兼容性模块。自动模块就是帮助旧项目逐步迁移到模块化,模块名由 JAR 文件名生成,如 commons-lang3.jar 自动生成的模块名为 commons.lang3

未命名模块(Unnamed Module)

类路径 (ClassPath)上的 JAR 或类文件集合,仍通过 -classpath 加载,这就是为了兼容传统类路径模式了,但仅对未命名模块和自动模块可见,应用模块无法直接访问其内容。

类型 来源 显式声明 访问权限 典型场景
系统模块 JDK/JRE 内置 无需 仅导出包可见 基础功能支持(如集合、IO)
应用模块 开发者自定义 必须 显式导出包 大型项目分层(如微服务模块)
自动模块 非模块化 JAR 文件转换 自动生成 默认导出所有包 过渡期依赖第三方库
未命名模块 类路径上的代码 仅限未命名模块间访问 遗留代码兼容

总结

扯来扯去,总算写完一篇文章了,其实还有模块相关命令没有讲,DeepSeek 问一下就出来,JDK9 模块总结一下就两点,强封装隔离机制显式依赖

对于 JDK9 模块语法来说,exportsopensprovides 都是开放可被其他模块依赖的包,exports 是完全放开,opens 是仅放开反射权限,而 provides 仅放开包中某个类的接口而已。

讲到这样,应该明白 JDK9 的模块有啥优势了,下面稍微讲一下三种对代码组织与复用的方式:

  • JDK 9 模块 :是 Java 9 引入的语言层面的概念,作为包的封装,是比关键字privatepublic 更上一层的封装。

  • Maven 模块 :是项目层面的划分,将一个大型项目按功能进行模块划分,用于管理大型项目的结构和依赖。

  • JAR 包:是一种文件格式,用于将多个 Java 类文件、资源文件和元数据文件归档成一个单独的文件,侧重于文件的打包和分发。

三者的核心目标之一都是帮助开发者更好地组织代码,提高代码的复用性。JAR 包将相关的类和资源打包在一起,方便在不同项目中复用;JDK 9 模块把功能相关的包组合成模块,实现代码的模块化复用;Maven 模块则是将大型项目拆分成多个小模块,各模块可独立开发、复用。

再提一句,和模块描述之外,还有一个为 Java 包提供文档注释的 package-info.java,要注意区分。

js 复制代码
/** 
  * 这是一个用于处理字符串操作的包。 
  * 它包含了一些实用的工具类,用于字符串的转换、验证等操作。 
  * 
  * @since 1.0 * @author John Doe 
 */ 
package com.example.stringutils;
相关推荐
冼紫菜3 分钟前
Java开发中使用 RabbitMQ 入门到进阶详解(含注解方式、JSON配置)
java·spring boot·后端·rabbitmq·springcloud
Kakikori3 分钟前
JSP链接MySQL8.0(Eclipse+Tomcat9.0+MySQL8.0)
java·开发语言
boring_1118 分钟前
Apache Pulsar 消息、流、存储的融合
分布式·后端
Dr.92715 分钟前
1-10 目录树
java·数据结构·算法
冬日枝丫24 分钟前
【spring】spring学习系列之六:spring的启动流程(下)
java·学习·spring
圈圈编码31 分钟前
LeetCode Hot100刷题——轮转数组
java·算法·leetcode·职场和发展
〆、风神31 分钟前
面试真题 - 高并发场景下Nginx如何优化
java·nginx·面试
應呈37 分钟前
FreeRTOS的学习记录(任务创建,任务挂起)
java·linux·学习
钢铁男儿39 分钟前
C# 深入理解类(静态函数成员)
java·开发语言·c#
bbsh209942 分钟前
动易.NET系列产品:Safari浏览器登录后台提示登录信息过期的问题
java·.net·safari