Java 9模块化解决“谁该用”问题,通过JPMS根治JAR Hell、public泛滥和启动臃肿;module-info.java是强制性模块声明文件,需置于源码根目录;必须使用--module-path而非-cp启动,否则退化为未命名模块。

模块化系统解决的不是“能不能用”,而是“谁该用、谁不该用”
Java 9 引入 JPMS(Java Platform Module System)根本不是为了新增功能,而是堵住类路径(classpath)长期放任自流导致的三个硬伤:
— JAR Hell:多个版本的 guava.jar 或 logback.jar 同时在类路径上,JVM 随机加载一个,运行时报 NoSuchMethodError 却编译不报错;
— public 泛滥:某个库内部包叫 com.example.internal.util,但只要它是 public class,任何模块都能 import 并调用,结果库一升级就崩;
— 启动臃肿:哪怕你只用 java.time,JVM 仍得把整个 rt.jar(或 JDK 9+ 的全部 60+ 个基础模块)全加载进内存。
module-info.java 是模块的“户口本”,不是可选配置文件
它必须放在每个模块源码根目录下,且命名、位置、语法都严格固定。常见误操作包括:
— 放在 src/main/java/com/example/mymodule/ 里(错!应放在 src/main/java/ 下);
— 忘记声明 requires java.base(其实可省略,但显式写上更清晰);
— 把本该 exports 的 API 包漏掉,导致其他模块编译报 package com.example.api is not visible;
— 误用 opens 替代 exports:前者只对反射开放(如 Hibernate 加载实体),后者才是让其他模块能正常 import 类。
一个最小可行示例:
module com.example.service {
requires java.base;
requires com.example.dto;
exports com.example.service.api;
}
模块路径(--module-path)和类路径(-cp)不能混用
这是迁移中最常踩的坑:一旦项目中出现 module-info.java,你就不能再靠 -cp 启动——否则所有模块都会退化为“未命名模块”(unnamed module),封装失效,依赖检查形同虚设。
正确做法是统一走模块路径:
— 编译:javac --module-path mods -d out src/**/*.java
— 运行:java --module-path out:mods -m com.example.app/com.example.app.Main
注意:mods 目录下必须是合法模块 JAR(含 module-info.class)或 exploded 模块结构;普通传统 JAR 丢进去会变成“自动模块”(automatic module),模块名从 JAR 文件名推导(如 guava-31.1-jre.jar → 模块名 guava),虽能用但失去强封装。
反射和框架兼容性是模块化落地最痛的点
Spring、Hibernate、JUnit 等重度依赖反射的框架,在模块化下默认被拦住——因为 com.example.entity.User 所在模块没声明 opens com.example.entity;,框架就无法通过 Class.getDeclaredFields() 访问私有字段。
解决方案只有两个:
— 在模块声明中加 opens(仅限需要反射的包,别 opens 整个模块);
— 或用 JVM 参数临时放开:--add-opens java.base/java.lang=ALL-UNNAMED(仅用于测试或遗留系统过渡)。
但要注意:--add-opens 不解决模块间访问问题,它只绕过 JVM 对反射的模块检查,不替代 exports。
立即学习“Java免费学习笔记(深入)”;
真正难的从来不是写 module-info.java,而是判断哪些包该 exports、哪些该 opens、哪些该用 requires transitive —— 这取决于你暴露的是 API、SPI 还是仅供测试的内部契约。边界划错,模块化就只剩形式。










