CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 进阶/课时/模块系统
RawBlame
课时目录12 节

Java 进阶

  1. 01Java 进阶概览
  2. 02枚举与模式
  3. 03Set/Queue/Stack
  4. 04OOP 进阶
  5. 05方法链与绑定
  6. 06Record 与不可变
  7. 07模块系统
  8. 08并发进阶
  9. 09依赖注入概念
  10. 10Java Web 框架
  11. 11Quarkus 入门
  12. 12综合练习

模块系统

JPMS module-path 概念。

30 分钟

模块系统

这一课你将能做到

  1. 区分 classpath 与 module-path,说清 JPMS 解决的三个问题(强封装、显式依赖、可裁剪运行时)
  2. 读懂并亲手写出含 exports / requires 的 module-info.java,编译运行一个双模块工程
  3. 分辨 Maven 多模块与 JPMS 模块是两个层面的概念,避免面试时混为一谈

为什么要学

Spring Boot 应用多数仍跑在 classpath 上,日常写业务几乎碰不到 module-info.java。但三类场景绕不开它:读 JDK 源码与文档(JDK 9 后 JDK 本身就是模块化的)、排查 InaccessibleObjectException/IllegalAccessError(框架反射访问 JDK 内部 API)、理解云原生时代 jlink 裁剪小体积运行时的思路。

面试角度:「classpath 和 module-path 的区别」「exports 和 opens 的区别」「为什么 Spring Boot 项目一般不写 module-info」是中频题,答出概念分层即可拉开差距。

先建立图像

Java 9 之前的世界:public 类对整个 classpath 全局可见——你的包结构只是「君子协定」,谁都能 import,JDK 内部 API(如 sun.misc.Unsafe)也能被误用。JPMS 加了一层 JVM 认可的边界:

                ┌─ module com.example.app ─┐
   requires ───→ │  只 import exports 过的包 │
                └───────────────────────────┘
                ┌─ module com.example.greeting ─┐
                │  exports com.example.greeting │ ← 对外可见
                │  com.example.internal         │ ← 对外不可见,javac 直接拒绝
                └───────────────────────────────┘

两个「模块」不要混:

层面是什么单位
Maven 多模块构建层:代码组织与依赖管理pom.xml 的 <modules>
JPMSJVM 层:运行时可见性与封装边界module-info.java

跟着做

步骤 1:建一个双模块工程目录

jmods-demo/
  src/
    com.example.greeting/
      module-info.java
      com/example/greeting/Greeting.java
    com.example.app/
      module-info.java
      com/example/app/App.java

src 下每个「模块名命名的目录」就是一个模块根,这是 JPMS 的标准源码布局。

步骤 2:编写两个模块

src/com.example.greeting/module-info.java:

module com.example.greeting {
    exports com.example.greeting;
}

src/com.example.greeting/com/example/greeting/Greeting.java:

package com.example.greeting;

public class Greeting {
    public static String hello(String name) {
        return "Hello, " + name + "!";
    }
}

src/com.example.app/module-info.java:

module com.example.app {
    requires com.example.greeting;
}

src/com.example.app/com/example/app/App.java:

package com.example.app;

import com.example.greeting.Greeting;

public class App {
    public static void main(String[] args) {
        System.out.println(Greeting.hello("JPMS"));
    }
}

步骤 3:模块化编译与运行

在 jmods-demo 目录执行(单行命令,Windows PowerShell 与 bash 通用):

javac -d out --module-source-path src src/com.example.greeting/module-info.java src/com.example.greeting/com/example/greeting/Greeting.java src/com.example.app/module-info.java src/com.example.app/com/example/app/App.java
java --module-path out --module com.example.app/com.example.app.App

输出:Hello, JPMS!。注意 --module 模块名/主类全名 的写法——module-path 世界里,主类靠「模块名 + 类名」定位,不再全 classpath 扫描。

步骤 4:体验强封装

在 Greeting.java 旁边加一个不导出的类 src/com.example.greeting/com/example/greeting/internal/Secret.java(package com.example.greeting.internal;,内容随意一个 public 类),然后回到 module-info.java——不要 export 这个包,直接在 App.java 里写 import com.example.greeting.internal.Secret; 再编译。

javac 直接报错:package com.example.greeting.internal is not visible。这就是编译期的边界强制,classpath 时代做不到。

步骤 5:查看 JDK 自身模块

java --list-modules
java --describe-module java.base

你会看到 java.base、java.sql、jdk.httpserver 等几十个 JDK 模块——JDK 9 后它自己是模块化的,这也是第 10 课能直接 import com.sun.net.httpserver 的原因。

讲清楚要点

  1. JPMS 的三个目标:强封装(未 export 的包对外不可见)、可靠配置(requires 显式声明依赖图,缺依赖启动即报错)、可定制运行时(jlink 按需裁剪出带应用的最小 JRE)。
  2. exports vs opens:exports 允许编译期 + 运行期直接访问;opens 只放开运行期反射访问(框架依赖注入、JSON 序列化需要),opens ... to xxx 还能限定只对指定模块开放。
  3. classpath 与 module-path 的关系:classpath 上的 jar 不感知模块边界(自动模块除外),行为与 Java 8 相同;module-path 上的 jar 必须有模块描述。两者可以共存,Boot 项目正是这么跑的。
  4. Spring Boot 为什么通常不写 module-info:生态内大量 jar 未模块化;Spring 的反射、动态代理、可选依赖在 module-path 下需要大量 opens/uses 配置,收益小于成本。全 classpath 是务实选择。
  5. 学习优先级:企业开发先掌握 Maven 多模块(构建层)与分层包结构;JPMS 是 JVM 层概念,能读懂、能排查反射报错即可,不必强行模块化业务项目。

你可能会懵的地方

  • requires static 和 requires transitive 是什么? requires static 是编译期需要、运行期可选(如 Lombok);requires transitive 是传递依赖——我依赖你时,也自动允许我的使用者依赖它指向的模块(如所有模块都该传递依赖 java.base)。
  • 没有 module-info 的 jar 放到 module-path 上会怎样? 成为「自动模块」:JDK 按文件名推导模块名并 export 所有包。名字不稳定、全导出弱化封装,实践中不推荐依赖这个行为。
  • 反射报 InaccessibleObjectException 怎么解? 典型原因是框架反射访问未 opens 的 JDK 包。启动参数加 --add-opens java.base/java.lang=ALL-UNNAMED 应急;正规解法等库本身适配模块化。
  • jlink 和 Docker 多阶段构建冲突吗? 不冲突:jlink 产出裁剪后的自定义运行时,放进更小的基础镜像(如 alpine/debian-slim),体积能从数百 MB 压到几十 MB;与 jva-quarkus 的 Native 思路殊途同归。
  • 包和模块什么关系? 模块是一组包的容器 + 可见性声明单元;一个包只能属于一个模块。包名重复但分属两个模块会直接冲突。

错法对照

错法问题正解
把 Maven <modules> 当成 JPMS两个层面,面试混淆会露馅Maven 管构建,JPMS 管运行时边界
给 Spring Boot 项目强上 module-info反射/可选依赖坑多,收益低全 classpath,理解概念即可
只 exports 不 opens 还抱怨框架反射失败框架靠运行期反射对反射方 opens ... to 目标模块
以为 public 就全局可见(Java 9+)module-path 下未导出包不可见记住两层可见性:public + exported
module-info 里忘写 requires 编译不过还删类是依赖声明缺失补 requires 而不是绕路

卡点急救

  1. 编译报「module not found」:--module-source-path 路径不对,或模块根目录名与 module 声明不一致;目录名必须精确等于模块名。
  2. 运行报「module xxx not found」:用了 classpath 的 java -cp 跑模块化应用;改 --module-path + --module。
  3. 想看某 jar 是不是模块:jar --describe-module --file xxx.jar;有 module-info 即显式模块,否则是普通/自动模块。

面试常问

  • classpath 与 module-path 的区别? 可背版:classpath 是 Java 8 以前的平坦搜索路径,public 即全局可见;module-path 是 Java 9+ 的模块容器,模块靠 module-info 声明 exports/requires,JVM 强制可见性边界,未导出的包即使 public 也访问不到;两者可共存,多数 Boot 应用跑在 classpath 上。
  • exports 与 opens 的区别? exports 放开的是「编译期 + 运行期的直接访问」;opens 放开的是「运行期的深度反射访问」(setAccessible、动态代理)。框架类库(Spring、Jackson)通常需要 opens;普通 API 依赖只需要 exports。
  • 为什么 Spring Boot 生态迟迟不全面模块化? 三点:海量第三方 jar 未模块化;框架核心机制是运行期反射与可选依赖,模块边界的严格性与之冲突;全 classpath 在工程上没有实际痛点。结论是「掌握概念、按需使用」,这正是技术选型题的标准答法。

自测三问

  1. JPMS 解决了哪三个问题?分别对应哪个指令?
  2. 框架反射访问 JDK 内部类报 InaccessibleObjectException,应急与正规解法各是什么?
  3. Maven 多模块和 JPMS 模块的核心区别是什么?

小结一句话

JPMS 是 JVM 层的封装与依赖边界,Boot 项目先全 classpath,但概念要懂、反射报错要会排查。

延伸

  • java.base 模块官方文档
  • 下一课:并发进阶
加载本节练习…
← 上一课Record 与不可变下一课 →并发进阶