Java 模块化系统 (JPMS):模块描述符与封装
Java 模块化系统 (JPMS):模块描述符与封装
引言:为什么需要模块化?
早在 Java 9 之前,Java 平台就面临着“JAR 地狱”和类路径 (Classpath) 导致的脆弱性问题。公共类即使不打算对外公开,也可能被任何代码随意访问,内部 API 被滥用,应用启动时因为缺失依赖而抛出 NoClassDefFoundError 更是家常便饭。Java 平台模块系统 (JPMS) 的引入从根本上解决了这些问题,它提供了比包 (package) 更强的封装和可靠的依赖管理。
本教程将带你掌握 JPMS 的核心概念,从零开始创建你的第一个模块化 Java 应用。
环境准备
确认安装成功后,在终端执行 java --version,你应该看到版本号 ≥ 9。
模块的核心:module-info.java
每个模块的根目录下必须包含一个名为 module-info.java 的文件,这便是模块描述符。它声明了模块的名称、依赖、导出和开放的包等信息。
一个最简单的模块描述符如下:
module com.example.greeting {
// 模块体目前为空
}
它声明了一个名为 com.example.greeting 的模块,但尚未导出任何包或依赖其他模块。接下来我们将逐步丰富它。
创建你的第一个模块
我们通过一个“问候”应用来学习。项目结构如下:
greeting-app/
├── com.example.greeting/
│ ├── module-info.java
│ └── com/example/greeting/
│ └── HelloWorld.java
└── com.example.client/
├── module-info.java
└── com/example/client/
└── Main.java
步骤一:编写模块 com.example.greeting
在目录 com.example.greeting/com/example/greeting/ 下创建 HelloWorld.java:
package com.example.greeting;
public class HelloWorld {
public static String getMessage() {
return "Hello, JPMS!";
}
}
这个类很简单,它提供了一个公共静态方法返回问候语。但此刻其他模块还无法访问它,因为我们还没有导出这个包。
步骤二:导出包
编辑 com.example.greeting/module-info.java:
module com.example.greeting {
exports com.example.greeting;
}
exports 关键字告诉模块系统:公开包 com.example.greeting 中的 public 类型,允许其他模块在编译时和运行时访问它们。注意,包内的非 public 类(如包级私有类)仍然对外不可见,这就是强封装。
步骤三:创建客户端模块并声明依赖
在 com.example.client/com/example/client/ 下创建 Main.java:
package com.example.client;
import com.example.greeting.HelloWorld;
public class Main {
public static void main(String[] args) {
System.out.println(HelloWorld.getMessage());
}
}
为了让 Main 能导入 HelloWorld,客户端模块必须要求 (requires) 问候模块。编辑 com.example.client/module-info.java:
module com.example.client {
requires com.example.greeting;
}
requires 表示该模块依赖另一个模块。现在模块系统会确保在编译和运行时,com.example.greeting 模块必须存在且可读。
步骤四:编译与运行
打开终端,进入 greeting-app/ 目录,执行以下命令。
编译模块:
# 创建输出目录
mkdir -p out
# 编译整个源码树,模块路径为当前目录
javac -d out --module-source-path . $(find . -name "*.java")
上面的命令使用 --module-source-path . 告诉编译器源代码按模块组织。编译后的 .class 文件会按模块结构存放在 out/ 下。
运行应用:
java --module-path out -m com.example.client/com.example.client.Main
--module-path (或 -p) 指定模块查找路径,-m 指定启动模块和主类。输出结果应为:
Hello, JPMS!
深入模块描述符
除了最基本的 exports 和 requires,模块描述符还支持多种高级指令,帮助我们精细控制访问和依赖。
限定导出:exports ... to
有时你希望将包的公开内容只暴露给特定的几个模块,而不对所有模块开放。例如,内部服务 API 只允许实现模块访问。
module com.example.service {
exports com.example.service.api;
exports com.example.service.internal to com.example.plugin;
}
这里 com.example.service.internal 仅被 com.example.plugin 模块可见,其他模块无权访问,即使它们 requires com.example.service。
传递依赖:requires transitive
当一个模块的公共 API 使用了另一个模块中的类型时,调用者通常也需要能读取那个传递依赖。使用 requires transitive 可以避免调用者重复声明。
module com.example.api {
requires transitive java.sql; // 因为API返回了java.sql.Date
exports com.example.api;
}
任何依赖 com.example.api 的模块会自动具有对 java.sql 模块的可读性,无需再次 requires java.sql。
静态依赖:requires static
该依赖在编译时是必需的,但在运行时是可选的。常用于注解或仅编译期处理的工具。
module com.example.util {
requires static java.compiler; // 编译时需要,运行时若未出现则忽略
}
服务消费与提供:uses 与 provides ... with
模块系统对 ServiceLoader 机制进行了原生支持。接口所在的模块可以通过 uses 声明服务消费,实现模块则通过 provides ... with 注册实现。
// 接口模块
module com.example.spi {
exports com.example.spi;
uses com.example.spi.Plugin;
}
// 实现模块
module com.example.plugin.csv {
requires com.example.spi;
provides com.example.spi.Plugin with com.example.plugin.csv.CsvPlugin;
}
运行时,ServiceLoader.load(Plugin.class) 会自动发现并加载提供的实现。
开放包用于反射:opens 与 open module
模块化强化了封装性,使得默认情况下对类型进行深度反射(如 setAccessible(true))会被禁止。如果框架(如 Hibernate、Jackson)需要通过反射访问你的实体类,你必须显式开放包。
module com.example.model {
// 允许对所有模块进行运行时反射访问
opens com.example.model.entities;
// 或仅对特定模块
opens com.example.model.dto to com.fasterxml.jackson.databind;
}
如果你希望整个模块的所有包都开放给反射(例如旧库迁移的过渡方案),可以将模块声明为 open module:
open module com.example.legacy {
requires spring.core;
// 所有包在运行时都可以被反射访问,但编译时仍遵循exports规则
}
封装的好处与最佳实践
JPMS 带来的强封装不仅是语法糖,它能有效:
- 隐藏内部实现:公共类若不导出其所在包,其他模块完全无法访问。这防止了外部代码依赖不稳定细节。
- 避免包分裂:同名包不能同时出现在多个模块中(原本在类路径下可能产生不可预知的 jar 顺序问题),模块系统会在启动时检测并报错。
- 更小的运行时镜像:借助
jlink工具,你可以构建只包含所需模块的自定义 JRE,极大缩小部署体积。
模块化开发的建议
- 从依赖最少的底层模块开始,逐层向上构建。
- 优先导出接口和抽象类所在的 API 包,实现类留在内部包中不导出。
- 测试模块化行为:运行
jdeps分析模块依赖,用jlink创建最小运行时来验证封装完整性。 - 库开发者应尽早模块化,通过
Automatic-Module-Name清单属性或完整的module-info为使用者提供可靠配置。
常见问题与排错
- 编译报错 “package is not visible”:检查是否导出了包,且调用方是否声明了
requires。 - 运行时报错 “module not found”:确认模块路径 (
--module-path) 包含了全部所依赖的模块目录或 JAR。 - 反射失败 “can not access a member of class ...”:目标包在模块中未被
opens给调用方模块。可通过--add-opens运行时选项临时解决,但推荐修改描述符。
结语
你现在已经掌握了 Java 模块化系统的核心武器:模块描述符和强封装。通过 module-info.java,你可以清晰地定义模块边界、控制对外可见性,并构建更安全、可维护的 Java 应用。模块化不再是可选项,而是现代 Java 开发的基石。动手实践,将你现有的小型项目迁移到模块系统,感受它带来的结构性优势吧!