Java 类路径与模块系统
classpath 如何定位类,JPMS 模块路径如何显式依赖与导出:两套模型的分工与迁移意识。
#concept
#growing
#dev
#java
[!info] related notes
Java 类路径与模块系统
这个概念为什么出现
JVM 启动后必须知道“到哪里加载类”。经典答案是 classpath;Java 9+ 引入 JPMS 模块系统,用 module-info 声明依赖与导出。现实世界两者并存:应用可能仍以 classpath 为主,JDK 自身强烈模块化。
[!abstract] 一句话理解 classpath 是类查找的历史主路径;模块系统用显式依赖与 exports/opens 强化封装;排障时要分清类在哪条路径、被谁加载。
最小可观察示例
场景:理解运行时如何指定 classpath
# 编译到 out 目录
mkdir -p out
printf 'public class Hi{public static void main(String[] a){System.out.println("hi");}}' > Hi.java
javac -d out Hi.java
java -cp out Hi
期望输出:
hi
模块示例(概念):
// module-info.java
module shop.app {
requires java.base; // 隐式也可,示意
exports shop.api;
}
结合场景再看三个关注点
-cp指向目录/JAR 列表。- classpath 目录指向包目录的根;例如
shop.Hi的 class 文件应在根下shop/Hi.class,源码位置则由构建方式决定。 - 模块化增加封装,反射默认可能需
opens。
核心概念与准确模型
1. classpath
- Linux/macOS 用冒号、Windows 用分号分隔目录与 JAR。
- 在应用加载器搜索 classpath 时,顺序会影响重复类的选择;父加载器委托和已加载的类也影响结果。
2. 模块路径
module-info.java:requires声明读取依赖,exports导出 public API,opens开放运行时深反射;provides/uses声明服务提供与使用。- 更强封装与依赖显式化
3. 自动模块与未命名模块
- 没有模块描述符的普通 JAR 放在 classpath 上,属于对应加载器的未命名模块;放在 module path 上并被解析后,可成为自动模块(名称来自清单或文件名)。
- 自动模块是有名字的模块,自动导出并开放其包;迁移到显式模块描述符后,需要重新声明访问边界。
4. 与构建工具
- Maven/Gradle 生成 classpath/module path
边界情况与反直觉行为
- 同名类不同 JAR → 隐蔽错误。
- 命名模块中的 split package 会受到解析与类加载限制,例如同一模块同时读取两个导出同名包的模块会冲突;不能直接沿用 classpath 中拼接多个 JAR 的做法。
- public API 反射不一律需要 opens;跨模块深反射非公开成员时通常才需要定向开放包。
常见误区
[!warning] 常见误区:认为模块系统可忽略 即便应用不写 module-info,也会与模块化 JDK/库交互(jlink、强封装)。
工程实践
- 依赖收敛,避免重复类。
- 可重现构建与锁定版本。
- 排障:
jdeps、-verbose:class。 - 新库评估是否支持 JPMS。
本节总结
- classpath 定位类;模块强化边界
- 冲突与封装是主战场
- 构建工具是日常入口
自测题
- 为什么依赖冲突会“同环境不同行为”?
- exports 的作用?
参考答案
- 不同机器/打包顺序导致 classpath 上胜出的类不同。
- 声明哪些包中的 public 类型/成员可由读取本模块的其他模块访问,也可定向导出;它不会同时开放私有成员的深反射。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Project Jigsaw / JPMS | 项目 | 模块系统 |
| java command | 工具 | -cp / module path |