Classpath 是什么
Classpath(类路径) 是 JVM 在运行时查找 .class 文件和 JAR 包的搜索路径集合。当代码里出现 new Foo() 或 Foo.class 时,类加载器要把 com.example.Foo 这个名字解析成磁盘上的 com/example/Foo.class 字节码,去哪里找,就由 classpath 决定。
可以把它理解成「类的 PATH 环境变量」:shell 的 PATH 决定 ls、git 这些命令去哪些目录找可执行文件;JVM 的 classpath 决定加载类时去哪些目录和 JAR 里找字节码。
一条 classpath 由多个条目组成,每个条目是一个目录或一个 JAR/ZIP 文件:
- 目录条目:以该目录为根,按包名展开找
.class(如条目target/classes+ 类com.foo.Bar→ 找target/classes/com/foo/Bar.class) - JAR 条目:把 JAR 当成一个打包的目录,在其中按同样规则找
分隔符与操作系统有关:Linux/macOS 用冒号 :,Windows 用分号 ;。
如何设置 classpath
按优先级从高到低,常见几种方式:
| 方式 | 写法 | 说明 |
|---|---|---|
-cp / -classpath |
java -cp app.jar:lib/* com.foo.Main |
命令行显式指定,最常用;详见 java -cp |
CLASSPATH 环境变量 |
export CLASSPATH=... |
全局生效,易污染,现代项目基本不用 |
可执行 JAR 的 Class-Path |
MANIFEST.MF 里的 Class-Path: |
JAR 内清单声明依赖;见 MANIFEST.MF |
| 默认值 | 不指定时为当前目录 . |
仅在没有任何设置时 |
-cp 支持通配符 lib/*(匹配该目录下所有 JAR,但不递归子目录),但不能写成 lib/*.jar。指定了 -cp 后默认的当前目录 . 会失效,需要的话要显式写进去。
classpath 与依赖管理工具
平时写 Maven/Gradle 项目,几乎不手写 classpath——构建工具替你算好了:
- Maven/Gradle 解析
pom.xml/build.gradle的依赖树,下载所有 JAR 到本地仓库 - 运行(
mvn exec、IDE Run、gradle bootRun)时,工具把「你的target/classes+ 全部依赖 JAR」拼成一条很长的 classpath 传给java
所以「某个依赖在不在 classpath 上」,本质就是「pom.xml 里有没有声明(且 scope 让它在运行期可见)这个依赖」。Spring Boot 的 @ConditionalOnClass 判断「classpath 上有没有某个类」,等价于在问「你引没引那个依赖 JAR」。
Spring Boot 打成 fat jar 后另有一套布局(BOOT-INF/lib/ + 自定义 JarLauncher),不走标准 Class-Path,细节见 Spring Boot Executable JAR。
classpath 与类加载器
classpath 是「去哪找」,类加载器是「怎么加载」。二者配合:AppClassLoader(System ClassLoader) 负责加载 classpath 上的类;核心库由 Bootstrap 加载,与 classpath 无关。双亲委托、命名空间等机制见 Java ClassLoader。
一个常见误区:「应用跑起来后所有操作都在读 classpath」是不对的。只有「首次加载某个类」或「读取 JAR 内资源」(如 getResourceAsStream)才走 classpath;类一旦加载进方法区、对象在堆上运行,普通业务逻辑不会反复扫 classpath。
Module Path(模块路径)
JDK 9 引入 JPMS(Java Platform Module System,模块系统) 后,多了一条与 classpath 并列的 module path(模块路径,--module-path / -p)。两者都是「找代码的路径」,但模型不同:
| 维度 | Classpath | Module Path |
|---|---|---|
| 引入版本 | 一直都有 | JDK 9+ |
| 单位 | 一堆松散的类 / JAR | 模块(带 module-info.class 的 JAR,或 automatic module) |
| 封装 | 无;任何 public 类都可被任意访问 | 强封装:只有 exports 的包对外可见 |
| 依赖声明 | 隐式(全靠 classpath 拼对) | 显式:module-info.java 里 requires |
| 缺依赖何时报错 | 运行期 NoClassDefFoundError |
启动期就校验模块图,缺失直接报错 |
| 重复/冲突 | 后者被遮蔽,易出诡异问题 | 同名模块冲突会启动失败,更早暴露 |
放在 classpath 上的普通 JAR 会进入一个特殊的 unnamed module(无名模块),能读取所有模块、行为基本和 JDK 8 一致——这是为了向后兼容。也就是说,你完全可以继续只用 classpath,不碰模块系统,绝大多数应用(包括大量 Spring Boot 项目)至今如此。
模块系统主要解决两类历史问题:
- 可靠配置:用
requires显式声明依赖,把「缺 JAR 才在运行期崩」提前到启动期校验,替代脆弱的 classpath。 - 强封装:用
exports控制哪些包对外可见,内部实现包(如sun.misc.*)真正藏起来,避免被随意调用。
JDK 自身从 JDK 9 起就模块化了(java.base、java.sql 等),这部分对所有程序生效;但你的应用要不要模块化是可选的。JPMS 的来龙去脉、module-info、automatic module 等见 JPMS / Project Jigsaw。
小结
- classpath 是 JVM 查找类与 JAR 的搜索路径,相当于「类的 PATH」。
- 日常由 Maven/Gradle 自动拼装;
@ConditionalOnClass等「类在不在」本质是问「依赖引没引」。 - 只有加载类 / 读 JAR 资源才用到 classpath,不是运行期一直在扫。
- module path 是 JDK 9+ 的并列机制,带强封装与显式依赖;不用它时 JAR 进入 unnamed module,行为同传统 classpath。