jar 打包、清单文件与 classpath
更新时间:2026-09-20。理解 jar 的物理结构和 classpath 解析规则,能解决一大批「本地能跑、线上找不到类」的问题。编译见 javac/java,工程化打包由 Maven/Gradle 完成,但它们产出的就是 jar。
一、jar 就是带清单的 zip
# 在编译输出目录操作(包结构要保留)
jar cf app.jar -C out . # c 创建 f 指定文件;把 out 下内容打进 app.jar
jar tf app.jar # t 列出内容(排查类在不在里面)
jar xf app.jar # x 解压
jar cfe app.jar com.demo.Main -C out . # e 直接指定入口主类(写入 Main-Class)解开后结构:
com/demo/Main.class
META-INF/MANIFEST.MF # 清单文件,描述入口与元数据二、MANIFEST.MF 与可执行 jar
手写清单 manifest.txt(最后要留空行):
Main-Class: com.demo.Main
Class-Path: lib/mysql.jar lib/util.jar打包并应用:
jar cfm app.jar manifest.txt -C out .
java -jar app.jarMain-Class让java -jar知道入口;Class-Path是相对于该 jar 所在目录的空格分隔列表,用来引用 jar 外部的依赖(jar 不会读自己内部嵌套的 jar)。
三、关键坑:-cp 在 -jar 时被忽略
java -cp lib/* -jar app.jar # ⚠️ -cp 会被 -jar 忽略!
java -cp "app.jar:lib/*" com.demo.Main # 这样才会合并 classpath用 java -jar 时,类路径只取 jar 清单里的 Class-Path,命令行 -cp 不生效。这是最常见的「我明明加了 -cp 还报找不到类」原因。
四、为什么不能把依赖 jar 直接塞进 jar
jar/JVM 不会递归加载「jar 里面的 jar」。如果你 jar uf app.jar lib/mysql.jar 把第三方 jar 当文件塞进去,运行时这些类不会出现在 classpath —— 这叫 fat/uber jar,必须靠专门手段:
- Maven shade / assembly、Gradle shadow:把依赖的
.class解开再合并进一个 jar; - Spring Boot 可执行 jar:自定义嵌套结构(
BOOT-INF/lib/*.jar+org.springframework.boot.loader启动器),由它的类加载器读嵌套 jar,不能用普通 Main-Class 直接 java 跑。
五、ClassNotFoundException vs NoClassDefFoundError
| 异常 | 含义 | 常见根因 |
|---|---|---|
ClassNotFoundException | 运行时按名加载但 classpath 里没有 | 漏依赖、-cp/清单 Class-Path 没配对 |
NoClassDefFoundError | 编译期有这个类,运行期初始化/链接时找不到或初始化失败 | scope 配成 provided/test、依赖被 exclusion、静态初始化抛异常 |
排查三步:jar tf app.jar | grep 类路径 确认类在不在产物里 → dependency:tree/dependencies 确认依赖有没有被裁掉 → 核对运行命令是 -jar(看清单 Class-Path)还是 -cp(看命令行)。多版本冲突常表现为 NoSuchMethodError,那是类在但版本不对。
六、模块化(module-info)的一点提示
用了 JPMS(module-info.java)后,类路径换成模块路径 --module-path/-p,还要 requires 声明依赖;没用 module-info 的 jar 仍走传统 classpath(无名模块)。多数服务端工程至今仍是 classpath 模式。
总结
jar 是带 META-INF/MANIFEST.MF 的 zip,用 jar cf/tf/xf 打包查看,清单里的 Main-Class 决定 java -jar 入口、Class-Path 以相对路径引用外部依赖;关键纪律是 java -jar 会忽略命令行 -cp,且 JVM 不加载嵌套在 jar 里的 jar,要合依赖须用 shade/shadow 做 fat jar 或用 Spring Boot 专用嵌套结构;排查时先 jar tf 确认类在不在、再查依赖树和 scope,区分「按名找不到」的 ClassNotFoundException 与「编译有运行缺失/初始化失败」的 NoClassDefFoundError,方法签名对不上的 NoSuchMethodError 则是版本冲突。