Skip to content

模块系统(JPMS)简介

概述

Java 平台模块系统(Java Platform Module System,简称 JPMS)是 Java 9 引入的一套模块化机制。它通过「模块」(module)把包(package)组织成有边界的单元,每个模块显式声明自己依赖谁requires)以及对外暴露什么exports),从而解决传统 classpath 下的「类路径地狱」、依赖隐藏和封装不足等问题。学习 JPMS 有助于理解 JDK 9+ 的目录结构、多版本 JAR 以及现代 Java 应用的模块化方式。

版本说明

JPMS 自 JDK 9 起可用。若你仍在使用 JDK 8,可将本文作为进阶阅读;实际模块化项目通常需 JDK 9+。


为什么需要模块系统

在 JDK 9 之前,Java 依赖 classpath:所有 JAR 平铺在一起,存在以下问题:

  1. 依赖不透明:无法从声明上看出一个 JAR 依赖了哪些其它 JAR,容易缺包或冲突。
  2. 封装不足:只要在 classpath 上,任何类都可以通过反射等访问其它 JAR 的「内部」API(如 sun.*com.sun.*),导致实现细节泄露。
  3. 类路径地狱:同名类、不同版本的类混在一起,难以管理和排查。

模块系统通过 module-info.java 让每个模块显式声明「我依赖谁」「我对外暴露哪些包」,实现强封装可分析的依赖图


模块描述符:module-info.java

每个模块在模块根目录下有一个 module-info.java 文件,称为模块描述符。它只包含模块声明,不是普通 Java 类。

基本语法

java
module 模块名 {
    // 依赖与导出声明
}
  • 模块名:通常与包名前缀一致(如 com.example.app),或与项目 artifact 名一致。
  • 模块名与包名没有强制绑定,但习惯上模块会包含若干包,并通过 exports 暴露其中一部分。

常见指令

指令说明
requires 模块名声明本模块依赖另一个模块(编译期和运行期都需要)。
requires static 模块名可选依赖:编译期需要,运行期可选(若不存在不会报错)。
exports 包名将指定包导出给所有其它模块使用。
exports 包名 to 模块1, 模块2仅将包导出给指定模块(限定导出)。
opens 包名开放包给反射使用(其他模块可反射访问该包内非 public 成员)。
opens 包名 to 模块1, 模块2仅对指定模块开放反射。
uses 服务接口声明本模块使用某服务接口(SPI)。
provides 服务接口 with 实现类声明本模块提供某服务的实现。

示例 1:最简单的模块

一个只依赖 JDK 基础模块并导出自己一个包的模块:

java
// module-info.java(位于模块根目录,与 com/ 等包目录同级)
module com.example.hello {
    requires java.base;           // 默认已依赖,可省略
    exports com.example.hello;     // 对外暴露 com.example.hello 包
}
  • java.base 是 JDK 的根模块,所有模块隐式 requires java.base,可写可不写。
  • 只有被 exports 的包里的 public 类型才能被其它模块访问;未 export 的包对外不可见(强封装)。

示例 2:多模块依赖

模块 A 依赖模块 B,并只把部分包导出:

java
// 模块 B:module-info.java
module com.example.utils {
    exports com.example.utils;
}

// 模块 A:module-info.java
module com.example.app {
    requires com.example.utils;      // 依赖 B
    exports com.example.app.api;     // 只导出 api 包,app.internal 不导出
}

其它模块只能使用 com.example.app.api 中的 public 类型;com.example.app.internal 即使为 public 也对其它模块不可见。


示例 3:opens 与反射

若框架(如 Spring、JPA)需要通过反射访问你的类(包括非 public 成员),则需用 opens 开放包,否则在 JDK 9+ 上可能抛出 InaccessibleObjectException

java
module com.example.web {
    requires com.example.app;
    requires spring.context;

    // 将 com.example.web 包开放给 Spring,以便反射创建 Bean、注入字段等
    opens com.example.web to spring.core;
}

提示

  • 只给需要反射的模块开放相应包,避免 opens 到「所有模块」以减小攻击面。
  • 若模块仅自己需要反射(如测试),可使用 opens 包名 to 本模块名

与 classpath 的对比

维度classpath(传统)模块系统(JPMS)
依赖隐式,靠「把 JAR 放进 classpath」显式 requires,缺模块会启动失败
可见性只要在 classpath 上,都可访问 public 类exports 的包中的 public 类型可被其它模块访问
反射默认可反射任意类opens 才能反射访问,否则可能报错
JDK 结构rt.jar 等大 JARJDK 自身被拆成模块(如 java.sql、java.xml)

注意事项

注意

  • 未命名模块:传统 classpath 上的 JAR 在 JDK 9+ 上会被视为「未命名模块」,它们可以读取所有模块,但被其它命名模块依赖时需注意兼容性。
  • 若项目尚未模块化,可继续使用 classpath;待有清晰边界和封装需求时再引入 module-info.java

注意

  • 模块名与包名不要混淆:模块名module-info.java 里声明;包名是类所在包。一个模块可包含多个包,只导出其中一部分。
  • 反射访问未 opens 的包在 JDK 9+ 会受限,若使用 Spring、JPA 等,需在相应模块中为框架所在模块做 opens

相关链接

基于 VitePress 构建