Michael Pratt 2024-01-08
1. 引言
随着越来越多的组织转向容器化和虚拟服务器,Docker 正在成为软件开发工作流中越来越重要的一部分。为此,Spring Boot 2.3 引入了一项令人兴奋的新功能:可以轻松地为 Spring Boot 应用程序创建 Docker 镜像。
在本教程中,我们将探讨如何为 Spring Boot 应用程序创建 Docker 镜像。
2. 传统的 Docker 构建方式
使用 Spring Boot 构建 Docker 镜像的传统方法是编写一个 Dockerfile。下面是一个简单的示例:
FROM openjdk:17-jdk-alpine
EXPOSE 8080
ARG JAR_FILE=target/demo-app-1.0.0.jar
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
然后我们可以使用 docker build 命令来构建 Docker 镜像。这种方式对大多数应用程序来说都能正常工作,但也存在一些缺点。
首先,我们使用的是 Spring Boot 生成的“fat jar”(即包含所有依赖的可执行 JAR 文件)。这可能会影响启动时间,尤其是在容器化环境中。通过将 JAR 文件解压后添加内容,我们可以减少启动时间。
其次,Docker 镜像是分层构建的。而 Spring Boot 的 fat jar 会将所有应用程序代码和第三方库打包到一个单独的层中。这意味着即使只修改了一行代码,整个层也必须重新构建。
如果在构建前先解压 JAR 文件,那么应用程序代码和第三方库就可以分别拥有自己的层。这样我们就能充分利用 Docker 的缓存机制:当仅有一行代码发生更改时,只需重新构建对应的那一层即可。
考虑到这一点,让我们看看 Spring Boot 是如何改进 Docker 镜像构建流程的。
3. Buildpacks(构建包)
Buildpacks 是一种提供框架和应用依赖的工具。
例如,给定一个 Spring Boot 的 fat jar,buildpack 会自动为我们提供 Java 运行时环境。这使我们无需编写 Dockerfile 就能自动生成一个合理的 Docker 镜像。
Spring Boot 同时支持 Maven 和 Gradle 使用 buildpacks。以 Maven 为例,我们可以运行以下命令:
./mvnw spring-boot:build-image
让我们看一下部分关键输出,了解背后发生了什么:
[INFO] Building jar: target/demo-0.0.1-SNAPSHOT.jar
...
[INFO] Building image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO] > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 100%
...
[INFO] [creator] ===> DETECTING
[INFO] [creator] 5 of 15 buildpacks participating
[INFO] [creator] paketo-buildpacks/bellsoft-liberica 2.8.1
[INFO] [creator] paketo-buildpacks/executable-jar 1.2.8
[INFO] [creator] paketo-buildpacks/apache-tomcat 1.3.1
[INFO] [creator] paketo-buildpacks/dist-zip 1.3.6
[INFO] [creator] paketo-buildpacks/spring-boot 1.9.1
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
[INFO] Total time: 44.796 s
第一行显示我们像往常一样构建了标准的 fat jar。
下一行开始构建 Docker 镜像。紧接着,我们看到它拉取了 Paketo 构建器镜像。
Paketo 是 Cloud Native Buildpacks 的一种实现。它会分析我们的项目,并自动确定所需的框架和库。在本例中,它识别出这是一个 Spring Boot 项目,并自动添加了必要的 buildpack。
最后,我们看到生成的 Docker 镜像和总构建时间。注意首次构建时,需要花费相当多的时间下载 buildpack 并创建不同的层。
buildpack 的一大优势在于生成的 Docker 镜像是多层结构。因此,如果我们只修改了应用程序代码,后续构建速度会显著加快:
...
[INFO] [creator] Reusing layer 'paketo-buildpacks/executable-jar:class-path'
[INFO] [creator] Reusing layer 'paketo-buildpacks/spring-boot:web-application-type'
...
[INFO] Successfully built image 'docker.io/library/demo:0.0.1-SNAPSHOT'
...
[INFO] Total time: 10.591 s
4. 分层 JAR(Layered JARs)
在某些情况下,我们可能不希望使用 buildpacks —— 也许我们的基础设施已经绑定到其他工具,或者我们已经有现成的自定义 Dockerfile 想要复用。
出于这些原因,Spring Boot 也支持使用分层 JAR的方式来构建 Docker 镜像。为了理解其工作原理,我们先看一下典型的 Spring Boot fat jar 的结构:
org/
springframework/
boot/
loader/
...
BOOT-INF/
classes/
...
lib/
...
fat jar 主要由三部分组成:
- 启动 Spring 应用所需的引导类(Bootstrap classes)
- 应用程序代码
- 第三方库
而在分层 JAR 中,结构类似,但会多出一个 layers.idx 文件,用于将 fat jar 中的每个目录映射到一个 Docker 层:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
Spring Boot 默认提供四个层:
- dependencies:来自第三方的常规依赖
- snapshot-dependencies:来自第三方的快照依赖
- resources:静态资源(注:默认配置中未显式列出,但可通过自定义启用)
- application:应用程序代码和资源
目标是将应用程序代码和第三方库放入反映其变更频率的独立层中。
例如,应用程序代码通常变更最频繁,因此被分配到独立的 application 层。这样,每一层都可以独立演进,只有当某一层发生变化时,Docker 镜像才需要重新构建该层。
现在我们了解了分层 JAR 的结构,接下来看看如何利用它来构建 Docker 镜像。
4.1 创建分层 JAR
首先,我们需要配置项目以生成分层 JAR。在 Maven 中,这意味着在 pom.xml 的 Spring Boot 插件部分添加如下配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
启用此配置后,执行 mvn package(及其依赖的命令)将生成一个使用上述四个默认层的分层 JAR。
4.2 查看和提取分层
接下来,我们需要从 JAR 中提取各层,以便在 Docker 镜像中正确分层。
要查看某个分层 JAR 的层结构,可运行以下命令:
java -Djarmode=layertools -jar demo-0.0.1.jar list
要提取这些层,可运行:
java -Djarmode=layertools -jar demo-0.0.1.jar extract
4.3 创建 Docker 镜像
将这些层集成到 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"]
这个 Dockerfile 首先从 fat jar 中提取各层,然后将每一层复制到最终的 Docker 镜像中。每个 COPY 指令都会在最终镜像中生成一个新的层。
构建该 Dockerfile 时,可以看到分层 JAR 中的每一层都被作为独立的 Docker 层添加:
...
Step 6/10 : COPY --from=builder dependencies/ ./
---> 2c631b8f9993
Step 7/10 : COPY --from=builder snapshot-dependencies/ ./
---> 26e8ceb86b7d
Step 8/10 : COPY --from=builder spring-boot-loader/ ./
---> 6dd9eaddad7f
Step 9/10 : COPY --from=builder application/ ./
---> dc80cc00a655
...
5. 结论
在本教程中,我们探讨了使用 Spring Boot 构建 Docker 镜像的多种方式:
- 使用 Buildpacks,我们可以无需编写模板代码或自定义配置,即可获得合适的 Docker 镜像;
- 或者,通过稍加努力使用 分层 JAR,我们可以构建出更精细、更高效的定制化 Docker 镜像。
这两种方法都显著优化了镜像构建效率和运行性能,尤其适合现代云原生应用场景。