1. 引言
Docker 已成为构建自包含应用的事实标准。从 Spring Boot 2.3.0 开始,框架引入了一系列增强功能,帮助我们更高效地构建 Docker 镜像。其中最值得关注的是 支持将应用拆分为多个分层(Layered Jars)。
简单来说,源代码被打包进独立的层中,这意味着当我们只修改了业务逻辑时,仅需重新构建这一层,从而显著提升构建效率和启动速度。本篇文章将带你了解如何利用 Spring Boot 的新特性来实现 Docker 镜像层的复用。
2. Docker 中的分层 JAR
Docker 容器由基础镜像和若干附加层构成。一旦某一层被构建完成,它就会被缓存下来,后续构建可直接复用,从而大幅提升构建速度:
需要注意的是,下层的变更会导致其上所有层都需要重新构建。因此,变动频率低的层应放在底层,而频繁变动的层则应放在顶层。
同样地,Spring Boot 支持将应用内容按层进行映射。默认的分层结构如下:
可以看到,应用代码被打包在独立的层中。当我们修改源码时,只有这一层需要重新构建,而 loader 和依赖层则保持缓存状态,从而减少镜像构建时间和启动时间。
3. 使用 Spring Boot 构建高效 Docker 镜像
传统的 Docker 镜像构建方式中,Spring Boot 使用的是 fat jar 模式,即将所有依赖和源码打包到一个文件中。这种方式的问题在于:哪怕只修改了一行代码,整个 jar 都需要重新构建。
3.1. Spring Boot 的分层支持
Spring Boot 2.3.0 引入了两个新特性来优化 Docker 镜像构建:
✅ **Buildpack 支持**:可自动构建包含 Java 运行时的 Docker 镜像,无需编写 Dockerfile
✅ **分层 JAR**:将依赖、资源、代码等划分为不同层,提升 Docker 构建效率
本篇文章主要讲解如何使用分层 JAR。
首先,我们需要在 Maven 中启用分层 JAR 构建功能。打包后,我们可以查看生成的 JAR 文件结构:
jar tf target/spring-boot-docker-0.0.1-SNAPSHOT.jar
你会发现在 BOOT-INF
目录下多了一个 layers.idx
文件:
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/"
3.2. 查看和提取分层
我们可以通过命令查看分层结构:
java -Djarmode=layertools -jar target/docker-spring-boot-0.0.1.jar list
输出结果为:
dependencies
spring-boot-loader
snapshot-dependencies
application
也可以将这些层提取到本地目录:
java -Djarmode=layertools -jar target/docker-spring-boot-0.0.1.jar extract
执行后你会看到如下目录结构:
$ ls
application/
snapshot-dependencies/
dependencies/
spring-boot-loader/
这些目录可以直接用于 Dockerfile 中,实现分层构建。
3.3. Dockerfile 配置
为了最大化利用 Docker 的缓存机制,我们需要将这些层逐个添加到镜像中。
Dockerfile 示例:
FROM openjdk:17-jdk-alpine as builder
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM openjdk:17-jdk-alpine
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
这样配置后,当我们只修改了源码时,仅 application
层会被重建,其余层都会复用缓存 ✅。
4. 自定义分层
默认分层虽然实用,但还不够精细。比如,所有依赖被打包进一个层,即使只是内部库有变更,也得重建整个依赖层 ❌。
4.1. 配置自定义分层
Spring Boot 支持通过配置文件来自定义分层结构。我们可以创建一个 layers.xml
文件来控制分层行为:
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
然后在 Maven 插件中指定该配置文件:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
</plugin>
4.2. 添加自定义层
假设我们想把内部依赖单独提取为一层:
<into layer="internal-dependencies">
<include>com.baeldung.docker:*:*</include>
</into>
并调整分层顺序:
<layerOrder>
<layer>internal-dependencies</layer>
</layerOrder>
打包后查看分层:
dependencies
spring-boot-loader
internal-dependencies
snapshot-dependencies
application
可以看到,新增了 internal-dependencies
层 ✅。
4.3. 更新 Dockerfile
提取后,在 Dockerfile 中添加新层:
COPY --from=builder internal-dependencies/ ./
构建镜像时,你会看到 Docker 单独构建了内部依赖层:
$ mvn package
$ docker build -f src/main/docker/Dockerfile . --tag spring-docker-demo
查看镜像历史:
$ docker history --format "{{.ID}} {{.CreatedBy}} {{.Size}}" spring-docker-demo
输出中会看到类似如下内容:
0e138e074118 /bin/sh -c #(nop) COPY dir:db6f791338cb4f209… 2.35kB
说明内部依赖层已成功构建 ✅。
5. 总结
本文介绍了如何使用 Spring Boot 构建更高效的 Docker 镜像,主要利用了分层 JAR 的特性:
- 对于简单项目,使用默认分层即可满足需求
- 对于复杂项目,可通过自定义分层策略进一步优化缓存复用
完整示例代码可在 GitHub 获取。