应采用分层构建而非直接复制fat jar:将JAR拆为dependencies、spring-boot-loader、snapshot-dependencies和application四层,按变更频率分离,结合多阶段Dockerfile与精确COPY指令,提升缓存复用率、减小镜像体积并加速CI/CD。

直接把 Spring Boot 的 fat jar 丢进 Docker 镜像里运行,看似简单,实则埋下性能、体积、构建效率三重隐患。真正高效的 Java 容器化,核心在于打破“单层胖包”惯性,转向按变更频率分层组织的构建逻辑——不是换工具,而是重构构建思维。
分层设计:按稳定性拆解 JAR 内容
Spring Boot 的 fat jar 表面是一个文件,内部却天然具备可分离结构。关键不是解压它,而是理解它的组成逻辑:
- dependencies 层:所有第三方 jar(spring-boot-starter-web、mybatis-spring-boot-starter 等),极少变动,应最早 COPY,最易命中缓存
- spring-boot-loader 层:Spring Boot 自带的启动类和资源加载器,版本锁定后基本不变
- snapshot-dependencies 层:公司内部快照模块(如 common-util:1.2.0-SNAPSHOT),开发期偶有更新,单独成层避免污染稳定层
- application 层:src/main/java + src/main/resources + application.yml 等业务代码,每次构建必变,必须放在最底层
多阶段构建:编译与运行环境彻底隔离
用一个 Dockerfile 同时完成编译和运行,等于把 maven、jdk、源码全塞进生产镜像——既不安全又臃肿。正确做法是两阶段切割:
- 第一阶段(builder)用 maven:3.8.8-eclipse-temurin-17,只做 clean package,不保留任何源码或中间产物
- 第二阶段(runtime)用 eclipse-temurin:17-jre-alpine 或 openjdk:17-jre-slim,仅 COPY target/*.jar 并解压,再按上述四层结构分别 COPY 解压后的内容目录
- 禁用
RUN java -jar,改用ENTRYPOINT ["java", "-cp", "app.jar:lib/*", "org.springframework.boot.loader.JarLauncher"],显式控制类路径
Dockerfile 编写关键细节
分层不是靠感觉,而要靠指令顺序和 COPY 粒度来保障:
立即学习“Java免费学习笔记(深入)”;
- 每层 COPY 必须独立成行,且路径精确——例如
COPY --from=builder app/BOOT-INF/lib/ /app/lib/对应 dependencies 层 - 在 runtime 阶段,先 RUN 创建空目录(如
RUN mkdir -p /app/lib),再 COPY,避免因路径不存在导致构建失败 - 务必配
.dockerignore:排除src/、pom.xml、target/、.git、README.md,防止无用文件污染构建上下文 - 禁用
ADD,统一用COPY;合并 RUN 命令(如RUN apt update && apt install -y curl && rm -rf /var/lib/apt/lists/*)减少层数
验证与收益落地
优化是否生效,不能只看 Dockerfile 多漂亮,得用数据说话:
- 执行
docker history your-image:tag,确认各层大小分布合理——dependencies 层应最大但最稳定,application 层最小但最常刷新 - 修改一行业务代码后重新 build,观察输出中 “Using cache” 行数:理想状态是前 3 层全部复用,仅最后一层重建
- 对比镜像体积:分层后 runtime 镜像通常比原始 fat jar 镜像小 15%~25%,启动耗时下降 20%+(JVM 类加载更直接)
- CI 流水线中开启 BuildKit(
DOCKER_BUILDKIT=1 docker build),支持更细粒度缓存和并行构建


















