模块系统通过显式声明依赖和导出解决JAR地狱问题,强制JVM启动时验证模块图;需正确配置module-info.java、模块路径及exports/opens指令。

模块系统解决了类路径混乱和隐式依赖问题
Java 9 引入的 module system 主要应对长期存在的“JAR 地狱”:同一类在多个 JAR 中重复出现、版本冲突、运行时才发现 NoClassDefFoundError 或 IllegalAccessError。它强制显式声明依赖(requires)和导出(exports),让 JVM 在启动时就能验证模块图,而不是等到反射调用或类加载失败才暴露问题。
典型表现是:IDE 显示一切正常,但打包成 jar 后运行报 java.lang.module.FindException: Module xxx not found——这说明构建时没把模块加入 --module-path,或者 module-info.java 里漏写了 requires。
标准 Java 模块工程结构必须包含 module-info.java
一个合法的命名模块(named module)必须在源码根目录(通常是 src/main/java)下有且仅有一个 module-info.java 文件。它不是可选配置,而是模块系统的入口契约。
常见错误包括:
立即学习“Java免费学习笔记(深入)”;
- 把
module-info.java放在src/main/java/com/example/下(应放在src/main/java/直接子目录) - 使用 Maven 构建时未设置
maven-compiler-plugin的release或source/target为 9+ - 模块名含大写字母或下划线(违反
Identifier规则,如my-module合法,MyModule不合法)
module com.example.app {
requires java.base;
requires javafx.controls;
exports com.example.app.ui;
}
模块路径(--module-path)和类路径(-cp)不能混用
一旦用了 --module-path,JVM 就进入“模块模式”,此时 -cp(即 CLASSPATH)会被忽略——所有依赖必须作为模块显式提供。这也是为什么 Spring Boot 2.3+ 默认不支持 JPMS:它的 fat jar 是扁平化类路径结构,与模块系统互斥。
调试时常用组合:
java --module-path mods --module com.example.app/com.example.app.Main- 若依赖未命名模块(legacy JAR),需用
--add-modules显式开启,例如:--add-modules java.xml.bind(Java 11 已移除) - 想临时开放模块内部包给非模块代码(如测试框架),用
--add-opens java.base/java.lang=ALL-UNNAMED
模块系统对传统分层架构的影响很实际
过去常见的 com.example.service 和 com.example.repository 包可以同属一个模块,但若拆成两个模块(com.example.service 和 com.example.data),就必须在 service 模块中写 requires com.example.data;,且 data 模块必须 exports com.example.data.api;(不能只 exports com.example.data; 内部实现包)。
这意味着:接口与实现分离不再是设计偏好,而是模块可见性的硬性要求。否则编译会报 error: package com.example.data.api is not visible。
容易被忽略的一点是:模块名 ≠ 包名。一个模块可以导出多个不同前缀的包,也可以不导出任何包(仅作依赖中介)。真正起作用的是 exports 和 opens 指令,不是目录结构本身。










