可执行 jar 里面到底是什么?为什么它启动比解压后的目录慢?分层 jar 对 Docker 镜像有什么用?为什么在 IDE 里好用的 getFile() 放进 jar 就报错?这篇打一个 Boot 4.0.8 的 jar,把它解开逐项实测。
mvn package 用时 3 秒;app.jar 大小 18.94 MB,共 168 个条目
META-INF/MANIFEST.MF:
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 21
Implementation-Title: app
Implementation-Version: 1.0.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: demo.Main
Spring-Boot-Version: 4.0.8
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
顶层结构(按目录汇总条目数):
BOOT-INF:1 个条目
BOOT-INF/classes:5 个条目
BOOT-INF/classpath.idx:1 个条目
BOOT-INF/layers.idx:1 个条目
BOOT-INF/lib:35 个条目
META-INF:1 个条目
META-INF/MANIFEST.MF:1 个条目
META-INF/maven:5 个条目
META-INF/services:2 个条目
org:116 个条目
BOOT-INF/lib 里嵌套的 jar:34 个,合计 18.7 MB;压缩方式:STORED(不压缩) × 34
BOOT-INF/classes 里的条目:data.txt、demo/Main.class、logback.xml
BOOT-INF/classpath.idx(34 行),前 6 行:
- "BOOT-INF/lib/commons-logging-1.3.6.jar"
- "BOOT-INF/lib/jackson-annotations-2.21.jar"
- "BOOT-INF/lib/jackson-core-3.1.5.jar"
- "BOOT-INF/lib/jackson-databind-3.1.5.jar"
- "BOOT-INF/lib/jakarta.annotation-api-3.0.0.jar"
- "BOOT-INF/lib/jspecify-1.0.1.jar"
BOOT-INF/layers.idx(10 行),前 6 行:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":BOOT-INF/classes 放自己的代码,BOOT-INF/lib 放不压缩的嵌套 jar,org/ 放启动器,MANIFEST 里 Main-Class 是 JarLauncher。
java -Djarmode=tools -jar app.jar extract --destination …:退出码 0;输出 ''
解出来的内容:app.jar、lib/;lib 目录里 34 个 jar
解出来的 app.jar 的 MANIFEST:Main-Class: demo.Main;Class-Path: lib/commons-logging-1.3.6.jar lib/jackson-annotations-2.21.jar lib/jackson-core-3.1.5.jar lib/jack
胖 jar:java -jar app.jar:中位 958 ms(范围 906~1051)
解压后的目录:java -cp BOOT-INF/classes:BOOT-INF/lib/* demo.Main:中位 805 ms(范围 777~823)
解压后的目录:java org.springframework.boot.loader.launch.JarLauncher:中位 812 ms(范围 803~846)
tools extract 之后:java -jar extracted/app.jar:中位 770 ms(范围 750~849)解压后再跑比直接跑胖 jar 快 150 到 190 ms(这个小应用);tools extract 得到的 app.jar 的 Main-Class 就是自己的主类。
list-layers(退出码 0):dependencies spring-boot-loader snapshot-dependencies application
每一层(文件数、大小、内容指纹前 10 位):
application:1 个文件,5 KB,指纹 84857bb5ce
dependencies:34 个文件,19192 KB,指纹 91f0390fc9
snapshot-dependencies:0 个文件,0 KB,指纹 e3b0c44298
spring-boot-loader:0 个文件,0 KB,指纹 e3b0c44298
只在应用里多加一个类、重新打包之后:
application:指纹变了(84857bb5ce → b374aaf948);大小 5 KB → 5 KB
dependencies:指纹没变;大小 19192 KB → 19192 KB
snapshot-dependencies:指纹没变;大小 0 KB → 0 KB
spring-boot-loader:指纹没变;大小 0 KB → 0 KB
新 app.jar 大小 18.94 MB;分层提取之后,没变的层(dependencies 等)在 Docker 里可以复用缓存,变的只有 application 层四层中 dependencies 体积最大且改代码后不变,application 只有几 KB,适合做 Docker 的分层缓存。
胖 jar:
Main 类的来源:jar:nested:<jtmp>/x77proj/target/app.jar/!BOOT-INF/classes/!/
Main 类的类加载器:org.springframework.boot.loader.launch.LaunchedClassLoader
java.class.path:<jtmp>/x77proj/target/app.jar
ClassPathResource(data.txt).getURL() = jar:nested:<jtmp>/x77proj/target/app.jar/!BOOT-INF/classes/!/data.txt
getFile() 抛 FileNotFoundException:class path resource [data.txt] cannot be resolved to absolute file path because it does not reside in the file system: jar:nested:<jtmp>/x77proj/target/app.jar/!BOOT-INF/classes/!/data.txt
getInputStream() 读到 19 个字符:hello from data.txt
解压后的目录:
Main 类的来源:file:<jtmp>/x77proj/target/exploded2/BOOT-INF/classes/
Main 类的类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader
java.class.path:<jtmp>/x77proj/target/exploded2/BOOT-INF/classes:<jtmp>/x77proj/target/e…(共 3689 个字符)
ClassPathResource(data.txt).getURL() = file:<jtmp>/x77proj/target/exploded2/BOOT-INF/classes/data.txt
getFile() = <jtmp>/x77proj/target/exploded2/BOOT-INF/classes/data.txt
getInputStream() 读到 19 个字符:hello from data.txt胖 jar 里 ClassPathResource.getFile() 抛 FileNotFoundException,getInputStream() 正常。