1. 引言

Docker 已成为构建自包含应用的事实标准。从 Spring Boot 2.3.0 开始,框架引入了一系列增强功能,帮助我们更高效地构建 Docker 镜像。其中最值得关注的是 支持将应用拆分为多个分层(Layered Jars)

简单来说,源代码被打包进独立的层中,这意味着当我们只修改了业务逻辑时,仅需重新构建这一层,从而显著提升构建效率和启动速度。本篇文章将带你了解如何利用 Spring Boot 的新特性来实现 Docker 镜像层的复用。

2. Docker 中的分层 JAR

Docker 容器由基础镜像和若干附加层构成。一旦某一层被构建完成,它就会被缓存下来,后续构建可直接复用,从而大幅提升构建速度:

docker layers

需要注意的是,下层的变更会导致其上所有层都需要重新构建。因此,变动频率低的层应放在底层,而频繁变动的层则应放在顶层。

同样地,Spring Boot 支持将应用内容按层进行映射。默认的分层结构如下:

spring boot layers

可以看到,应用代码被打包在独立的层中。当我们修改源码时,只有这一层需要重新构建,而 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 获取。


原始标题:Reusing Docker Layers with Spring Boot