摘要:本文主要介绍Spring Boot项目的docker镜像分层构建,分为外部依赖和内部依赖和项目classes依赖,让docker构建的时候,可以使用之前的缓存,加快构建速度和测试环境镜像拉取速度。
为什么使用分层 Docker 镜像?
传统的 Docker 镜像构建方式会将整个应用程序 JAR/WAR 文件复制到一个层中。当你修改代码时,整个层都会失效,导致 Docker 必须重新构建整个应用程序层,即使只是做了微小的代码更改。
通过我们的分层方法,我们将以下内容分离:
- 外部依赖(第三方库)
- 内部依赖(公司特定库)
- 应用程序代码(实际业务逻辑)
这样,当你修改代码时,只需要重新构建应用程序代码层,而依赖层可以从缓存中重用。
Maven 插件配置详解
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-external-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/external</outputDirectory>
<includeScope>compile</includeScope>
<excludeGroupIds>cn.hutool</excludeGroupIds>
</configuration>
</execution>
<execution>
<id>copy-internal-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/internal</outputDirectory>
<includeScope>compile</includeScope>
<includeGroupIds>cn.hutool</includeGroupIds>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<!-- 如果使用预览特性 -->
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
<release>21</release>
</configuration>
</plugin>
</plugins>
</build>
工作原理
1. Maven 依赖分离插件配置详解
我们的 pom.xml 使用 maven-dependency-plugin 在构建过程中分离依赖。该插件包含两个执行配置:
外部依赖复制配置
xml
<execution>
<id>copy-external-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/external</outputDirectory>
<includeScope>compile</includeScope>
<excludeGroupIds>cn.hutool</excludeGroupIds>
</configuration>
</execution>
配置参数说明:
<id>: 执行的唯一标识符<phase>: 绑定到 Maven 生命周期的 package 阶段<goals>: 执行的目标是 copy-dependencies<configuration>: 配置详情
-
<outputDirectory>: 输出目录,${project.build.directory}是 target 目录<includeScope>: 包含作用域为 compile 的依赖<excludeGroupIds>: 排除 groupId 为 cn.hutool 的依赖(将其视为内部依赖)
内部依赖复制配置
xml
<execution>
<id>copy-internal-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib/internal</outputDirectory>
<includeScope>compile</includeScope>
<includeGroupIds>cn.hutool</includeGroupIds>
</configuration>
</execution>
配置参数说明:
<id>: 执行的唯一标识符<phase>: 同样绑定到 Maven 生命周期的 package 阶段<goals>: 执行的目标也是 copy-dependencies<configuration>: 配置详情
-
<outputDirectory>: 输出目录为 target/lib/internal<includeScope>: 包含作用域为 compile 的依赖<includeGroupIds>: 只包含 groupId 为 cn.hutool 的依赖(作为内部依赖处理)
通过这两个执行配置,Maven 会在 package 阶段自动将依赖分别复制到对应的目录中:
target/lib/external/- 第三方依赖(排除了 cn.hutool)target/lib/internal/- 内部/公司特定依赖(只有 cn.hutool)target/classes/- 编译后的应用程序代码
2. 分层 Dockerfile 详解
我们的 Dockerfile 通过首先复制依赖来利用 Docker 层缓存:
bash
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 1. 复制外部依赖(很少更改)
COPY target/lib/external/*.jar ./lib/external/
# 2. 复制内部依赖(偶尔更改)
COPY target/lib/internal/*.jar ./lib/internal/
# 3. 复制应用程序类(经常更改)
COPY target/classes ./classes
EXPOSE 8080
ENV CUSTOM_OPTS="-Xms256m -Xmx768m"
ENV JAVA_OPTS="--enable-preview -XX:+UseSerialGC"
ENTRYPOINT ["sh", "-c", "java $CUSTOM_OPTS $JAVA_OPTS -cp /app/classes:/app/lib/external/*:/app/lib/internal/* com.example.layer.Sp3LayerApplication"]
Dockerfile 配置说明:
FROM eclipse-temurin:21-jre-alpine: 使用轻量级的 Alpine Linux 上的 Temurin JDK 21 运行时环境WORKDIR /app: 设置工作目录为 /appCOPY target/lib/external/*.jar ./lib/external/: 将外部依赖复制到镜像中,这是最稳定的层COPY target/lib/internal/*.jar ./lib/internal/: 将内部依赖复制到镜像中,这层比外部依赖更可能变化COPY target/classes ./classes: 将应用程序类文件复制到镜像中,这层最容易发生变化EXPOSE 8080: 暴露应用程序端口ENV指令设置 JVM 参数:
-
CUSTOM_OPTS: 自定义 JVM 参数,设置初始堆内存和最大堆内存JAVA_OPTS: Java 虚拟机选项,启用预览特性和使用串行垃圾收集器
ENTRYPOINT: 定义容器启动时执行的命令,使用类路径包含所有依赖和应用程序类
类路径说明:
-cp /app/classes:/app/lib/external/*:/app/lib/internal/* 指定了类路径的顺序:
/app/classes: 应用程序编译后的类文件优先级最高/app/lib/external/*: 外部依赖次之/app/lib/internal/*: 内部依赖最后
这种顺序确保了应用程序类可以覆盖依赖中的同名类(如果需要的话),并且保证了正确的类加载顺序。
3. 构建过程详解
Maven 构建生命周期集成
当执行 mvn package 命令时,Maven 会按照以下顺序执行构建阶段:
validate- 验证项目正确性compile- 编译源代码test- 运行测试package- 打包应用程序
在 package 阶段,maven-dependency-plugin 会自动执行我们配置的两个依赖复制任务:
- 首先执行
copy-external-dependencies,将外部依赖复制到target/lib/external/ - 然后执行
copy-internal-dependencies,将内部依赖复制到target/lib/internal/ - 同时,Maven 默认会将编译后的类文件放在
target/classes/目录中
完整构建步骤
- 清理并打包应用程序:
go
mvn clean package
此命令会触发依赖分离过程,生成三个目录:
-
target/classes/- 应用程序编译后的类文件target/lib/external/- 外部依赖 JAR 文件target/lib/internal/- 内部依赖 JAR 文件
- 构建 Docker 镜像:
erlang
docker build -t sp3-layer .
Docker 构建过程会按顺序创建四个层:
-
- 基础镜像层(eclipse-temurin:21-jre-alpine)
- 外部依赖层(COPY target/lib/external/*.jar)
- 内部依赖层(COPY target/lib/internal/*.jar)
- 应用程序类层(COPY target/classes)
构建优化效果
由于 Docker 的层缓存机制:
- 当只修改应用程序代码时,只有最后一层需要重建
- 当添加新的外部依赖时,需要重建外部依赖层及其后的所有层
- 当添加新的内部依赖时,需要重建内部依赖层和应用程序类层
- 只有基础镜像变更时,才需要完全重建
优势
- 更快的构建速度:代码更改时只需重新构建应用程序代码层
- 更小的传输量:推送到注册中心时,只传输更改的层
- 更好的缓存效果:依赖层在各个构建之间缓存并重用
- 减少带宽消耗:在 CI/CD 流水线中减少数据传输
版本更新工作流程
- 在应用程序中进行代码更改
- 运行
mvn clean package仅重新编译更改的代码 - 运行
docker build -t sp3-layer:v2 .构建新镜像 - 只有
target/classes层会被重新构建 - 只将更改的层推送到容器注册中心
配置自定义指南
要将此配置应用于其他项目,您需要根据项目的依赖结构调整配置:
修改依赖分组
如果您有不同的内部依赖,需要修改 excludeGroupIds 和 includeGroupIds 配置:
xml
<!-- 外部依赖配置 -->
<excludeGroupIds>com.yourcompany,org.another.internal.group</excludeGroupIds>
<!-- 内部依赖配置 -->
<includeGroupIds>com.yourcompany,org.another.internal.group</includeGroupIds>
最佳实践
- 顺序很重要:首先复制不经常更改的层
- 最小化层数:只分离实际在不同频率下更改的内容
- 多阶段构建:考虑与多阶段构建结合使用以获得更小的最终镜像
- 缓存失效:了解 Docker 的层缓存机制以最大化收益
这种方法可以显著减少 CI/CD 流水线中的构建时间,并在部署更新时最小化网络传输。