在云原生时代的日常开发中,CI/CD流水线的效率直接影响着团队的交付节奏。你是否遇到过这样的困境:仅因为修复了一个小Bug,修改了一行业务代码,就需要重新构建并上传一个体积庞大的完整Docker镜像,在漫长的上传等待中消耗宝贵的发布窗口?
背后的原因在于传统的构建方式:将Spring Boot打包生成的Fat Jar整体COPY进Docker镜像。这种方式未能充分利用Docker镜像的分层存储机制,导致每次代码变更都需处理整个Jar包,造成带宽与时间的双重浪费。
本文将介绍如何利用Spring Boot 2.3+官方提供的layertools,实现镜像的分层构建,将更新镜像从“整体搬运”变为“局部更新”,从而显著提升构建与发布效率。
传统“物理瘦身”的局限性
Docker镜像采用分层存储结构。当将一个完整的、约200MB的Fat Jar复制到镜像中时,Docker将其视为一个独立的层。一旦Jar包内的任何文件发生变更(例如业务代码),这一整个层都会失效,需要重新构建和上传。
然而,分析一个典型的Spring Boot应用,其构成大致可分为两部分:
- 依赖项:包括Spring框架自身及各类第三方库。这部分通常占据Jar包体积的90%以上,但变更频率极低。
- 应用代码:开发者编写的业务逻辑,如Controller、Service等。这部分体积小(可能仅几百KB),但会频繁变更。
核心矛盾由此产生:为了一小部分频繁变动的代码,不得不反复上传绝大部分几乎不变的依赖,这是对CI/CD流程效率的严重损害。
Spring Boot Layered Jars 解决方案
自Spring Boot 2.3版本起,官方引入了layertools模式来根治此问题。其核心思想是:在打包阶段就对Jar包内容进行分类,并依据变更频率将其规划到不同的Docker镜像层中。

第一步:启用并验证分层Jar包
首先,确保你的项目已启用分层支持。在pom.xml中配置spring-boot-maven-plugin(此配置通常已默认开启):
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
执行mvn clean package后,生成的Jar包内部已包含分层索引信息。可以通过以下命令查看预定义的分层:
java -Djarmode=layertools -jar your-application.jar list
输出将显示类似的分层名称:
dependencies (第三方稳定依赖)
spring-boot-loader (Spring Boot类加载器)
snapshot-dependencies (快照版本依赖)
application (你的应用代码与资源)

核心原理:分层顺序由谁定义?
你可能会疑惑,Spring Boot如何确定文件归属?分层的顺序又依据什么?
关键在于打包后生成的BOOT-INF/layers.idx文件。它是一份明确的“分层清单”,定义了Jar包内文件的归属与层级关系。可以直接查看其内容:
# 解压并查看 layers.idx
jar xf target/your-app.jar BOOT-INF/layers.idx
cat BOOT-INF/layers.idx

文件内容示例如下:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
这个顺序至关重要。它列出了Jar包中的逻辑层次。在后续构建Docker镜像时,我们必须按照从最稳定到最易变的顺序(即dependencies -> spring-boot-loader -> snapshot-dependencies -> application)将各层复制到镜像中。这样,稳定的底层就能被Docker缓存高效复用,只有顶层的应用代码层会因变更而重建。
第二步:编写多阶段构建Dockerfile
我们通过Docker的多阶段构建来实现分层提取与组装:
- Builder阶段:提取Jar包中的各层文件。
- Runner阶段:按顺序将各层复制到运行镜像中。
以下是完整的Dockerfile示例:
# === Stage 1: 构建与提取层 ===
FROM eclipse-temurin:21-jdk-alpine as builder
WORKDIR /application
# 传入构建好的Jar包
ARG JAR_FILE=target/your-application.jar
COPY ${JAR_FILE} application.jar
# 使用 layertools 提取分层文件
# 执行后会生成 dependencies/, spring-boot-loader/, application/ 等目录
RUN java -Djarmode=layertools -jar application.jar extract
# === Stage 2: 运行镜像 ===
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /application
# 按依赖稳定性从低到高复制,最大化利用Docker缓存
# 1. 依赖层(体积大,极少变动)
COPY --from=builder /application/dependencies/ ./
# 2. 快照依赖层(偶尔变动)
COPY --from=builder /application/snapshot-dependencies/ ./
# 3. Spring Boot加载器层(基本不变)
COPY --from=builder /application/spring-boot-loader/ ./
# 4. 应用层(频繁变动,体积小)
COPY --from=builder /application/application/ ./
# 使用JarLauncher启动
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
效能提升对比
首次构建时,由于需要下载所有基础镜像并构建每一层,耗时与传统方式相近。
但当仅修改业务代码后再次构建时,效果立现:
- Docker检测到
dependencies、spring-boot-loader等层的构建指令和源文件未变,直接使用缓存。
- 仅
application层因文件变更而需要重新构建和打包。
- 推送镜像到仓库时,仅需上传最新的、体积很小的
application层。
最终收益:
- 构建速度:从几分钟降至几十秒。
- 上传带宽:从数百MB降至几十KB,发布瞬间完成。
- 仓库存储:底层依赖层被多个镜像版本共享,节约大量磁盘空间。
总结
对SpringBoot应用进行容器化分层构建,并非高深莫测的复杂技术,而是对现有工具链(Spring Boot layertools)和容器原理(Docker分层缓存)的深度理解和巧妙运用。通过将“一成不变”的依赖与“瞬息万变”的代码分离到不同的镜像层,可以实质性地优化CI/CD流水线的效率,降低运维成本。这是一种典型的通过工程化实践提升研发效能的思路。