Spring Boot Fat JAR 容器化指南

Spring Boot Fat JAR 容器化指南

背景

传统 Spring Boot 项目通常以 Fat JAR 形式交付,通过 java -jar app.jar 直接运行。随着容器化的普及,越来越多的场景要求以 Docker 镜像形式交付,或在开发测试阶段使用容器环境。但是往往又不希望或者没有资源大范围或者大幅度修改已有基础设施以适配。该文档基于该需求给出一种可行的解决方案。

这是个兼容或过渡指南,如果你框架版本允许且能修改现有构建过程的话请使用官方的分层 Jar 或者直接打包镜像

传统方案及其问题

最直接的容器化方式是将 JAR 整体打入镜像:

dockerfile 复制代码
FROM eclipse-temurin:8-jre
WORKDIR /app
COPY target/app.jar .
ENTRYPOINT ["java", "-jar", "app.jar"]

该机制存在明显的效率缺陷,由于 Docker 镜像构建采用分层缓存机制,jar 作为一个整体文件导致缓存粒度过粗,内部任何微小的应用代码或配置文件变更,都会导致整个包含数百兆第三方依赖的镜像层缓存失效。这直接导致每次构建需全量传输数据,造成构建周期延长、存储冗余以及网络带宽的无效消耗。且配置文件查看修改映射也极为不便。

基于物理结构的解压与分层原理

Fat JAR 内部结构

为在不修改现有项目构建配置的前提下解决效率问题,可通过分析应用程序的内部物理结构,实施基于多阶段构建的手动解压与分层策略。

自 Spring Boot 1.4 版本起,其可执行归档文件内部采用了标准化的目录结构。核心组件被物理隔离在不同的层级中,结构如下:

复制代码
app.jar
├── BOOT-INF/
│   ├── classes/      # 应用程序自身的编译字节码与配置文件,该部分体积较小但随开发迭代频繁变更
│   └── lib/          # 所有第三方依赖包,该部分体积最大且变更频率极低
├── META-INF/         # 框架的元数据
└── org/              # 框架的类加载器

利用此结构特征,可在容器构建阶段将归档文件拆解,并依据各目录的变更频率顺序构建镜像层。

构建流程

实现此策略依赖容器的多阶段构建特性,过程分为解压提取与分层组装两个阶段。

在解压提取阶段,引入包含完整 Java 开发工具包的基础镜像作为构建器,利用系统自带的归档管理工具将编译产物解压至指定的工作目录。此步骤与 Spring Boot 自身的运行机制解耦,构建环境无需与应用程序最终的运行版本严格一致,具备标准的解压能力即可。

在分层组装阶段,采用仅包含 Java 运行环境的轻量级基础镜像。根据缓存命中率最大化原则,从解压目录中按变更频率由低至高的顺序将文件复制到最终镜像。第三方依赖最先被复制,随后是引导程序与元数据,最后是应用程序字节码。在指定容器启动命令时,需避开传统的启动方式,改为直接调用 Spring Boot 内部的类加载器。

注意:启动类的路径受框架版本影响

对于 Spring Boot 1.4 至 3.1 版本,需指定为 org.springframework.boot.loader.JarLauncher

对于 Spring Boot 3.2 及更高版本,需指定为 org.springframework.boot.loader.launch.JarLauncher。

完整的容器化构建配置如下所示:

dockerfile 复制代码
# 阶段 1:解压提取
FROM eclipse-temurin:8-jdk AS builder
ARG JAR_FILE=app.jar
WORKDIR /app
COPY ./target/${JAR_FILE} ./app.jar
RUN mkdir -p exploded && cd exploded && jar -xf ../app.jar

# 阶段 2:分层组装
FROM eclipse-temurin:8-jre
WORKDIR /app

# 依据变更频率由低至高复制文件
COPY --from=builder /app/exploded/BOOT-INF/lib/ ./BOOT-INF/lib/
COPY --from=builder /app/exploded/org/ ./org/
COPY --from=builder /app/exploded/META-INF/ ./META-INF/
COPY --from=builder /app/exploded/BOOT-INF/classes/ ./BOOT-INF/classes/

# 适用于 Spring Boot 1.4 ~ 3.1 版本的启动配置
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

# 适用于 Spring Boot 3.2+ 版本的启动配置需替换为:
# ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

相对传统方案的优点

  • 依赖库层可被缓存复用,仅业务代码层需要重新构建和传输
  • 配置文件位于 BOOT-INF/classes/ 层,可单独更新或替换而不影响依赖层
  • 构建和推送时间显著缩短

快照依赖的精细化隔离策略

在包含频繁变动快照版本依赖的项目环境中,整体复制依赖目录仍可能引发非预期的缓存失效。为进一步精细化分层,可在解压提取阶段对依赖目录进行二次处理,识别并剥离带有快照依赖至独立目录。在分层组装阶段,依次复制稳定版依赖目录与快照版依赖目录。这种分离操作可确保稳定版依赖层的缓存不被日常联调破坏,示例如下:

dockerfile 复制代码
# 在 builder 阶段分离快照依赖
RUN mkdir -p exploded/BOOT-INF/lib-snapshot \
    && find exploded/BOOT-INF/lib/ -name "*-SNAPSHOT.jar" -exec mv {} exploded/BOOT-INF/lib-snapshot/ \;

# 在最终镜像阶段依次拷贝
COPY --from=builder /app/exploded/BOOT-INF/lib/ ./BOOT-INF/lib/
COPY --from=builder /app/exploded/BOOT-INF/lib-snapshot/ ./BOOT-INF/lib/

方案对比与适用范围

自 2.3 版本起,Spring Boot 官方框架引入了原生分层支持,通过配置构建插件即可生成包含分层索引的归档文件,并利用特定的系统参数提取出逻辑层。虽然官方方案提供了标准化的集成路径,但上述手动解压分层策略仍具有特定优势。

该策略具有完全的非侵入性不依赖特定 Spring Boot 版本,且无需对应用程序的源代码或项目构建配置文件进行修改。适用于

  1. 无法升级框架版本
  2. 无法修改或全面修改现有构建配置
  3. 处于项目容器化升级过渡期
  4. fatjar 来源于三方交付无法控制其构建方式

的项目

相关推荐
Mr.朱鹏8 小时前
JVM-GC垃圾回收案例
java·jvm·spring boot·算法·spring·spring cloud·java-ee
Nan_Shu_6148 小时前
学习: 尚硅谷Java项目之小谷充电宝(3)
java·后端·学习
智能工业品检测-奇妙智能8 小时前
AIFlowy如何实现与现有Spring Boot项目的无缝集成?
java·spring boot·后端
Ama_tor9 小时前
Flask零基础进阶(中)
后端·python·flask
人道领域9 小时前
苍穹外卖:菜品新增功能全流程解析
数据库·后端·状态模式
野犬寒鸦9 小时前
TCP协议核心:TCP详细图解及TCP与UDP核心区别对比(附实战解析)
服务器·网络·数据库·后端·面试
一知半解仙9 小时前
从“玩具项目“到“生产级架构“:Spring Boot + Spring Cloud + AI 微服务实战避坑指南
spring boot·spring cloud·架构
毕设源码-朱学姐9 小时前
【开题答辩全过程】以 基于springBoot微服务架构的老年人社交系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Mr.朱鹏9 小时前
分布式-redis主从复制架构
java·spring boot·redis·分布式·缓存·架构·java-ee