bigywater.tech
Spring · 77

可执行 jar 的结构与分层 jar

可执行 jar 里面到底是什么?为什么它启动比解压后的目录慢?分层 jar 对 Docker 镜像有什么用?为什么在 IDE 里好用的 getFile() 放进 jar 就报错?这篇打一个 Boot 4.0.8 的 jar,把它解开逐项实测。

JarLauncher
MANIFEST 的 Main-Class 是 JarLauncher,你自己的主类放在 Start-Class
19192 KB 不变 / 5 KB 变
改一个类之后,dependencies 层指纹不变,application 层指纹变化
958 → 770 ms
胖 jar 与 tools extract 之后的 app.jar 的启动时间中位数

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() 正常。

分步演示

可执行 jar
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 mvn package(spring-boot-starter-parent 4.0.8、spring-boot-maven-plugin 由它管理版本)打出一个 webmvc 应用的 app.jar,jar 的内容用 Python zipfile 解开读取;启动时间的计时方式是进程启动到 ApplicationReadyEvent 之后立即 System.exit(0),每种跑法 7 次取中位数,没有真正对外提供服务;分层的指纹是对每个层目录里全部文件的相对路径和内容做 SHA-256 后取前 10 位
    • 没有验证:Dockerfile 里真正用分层 COPY 构建镜像并比较构建时间与镜像大小;extract 的 --launcher 参数(本次没带,spring-boot-loader 层是空的);自定义 layers.xml;Gradle 的 bootJar;含 SNAPSHOT 依赖时 snapshot-dependencies 层的行为;胖 jar 慢的具体原因(只测了时间,没有分解);Windows 路径;CDS 与可执行 jar 配合(见第 74 篇)
    京ICP备2026067502号-1京公网安备11010502063487号