Loading

基于Gradle和Spring Boot的分层Jar Docker镜像打包

背景

笔者负责的项目部署在某某云之上,内外网完全隔离,通过一个带宽10M的小水管访问项目服务器,项目后端使用Spring Boot + Docker构建,构建后的服务运行在k8s中;应用构建使用公司本地服务器的Jenkins实现(需要访问公司的Gitlab、Nexus仓库和外网的Maven镜像),镜像构建使用项目内网环境中部署的Jenkins实现(构建完成后推送到Harbor以供k8s使用)。

工具和框架版本

名称 版本
Gradle 7.5.1
Spring Boot 2.6.13
JDK Open JDK 11

痛点

背景中描述的CICD流程虽然可以正常运作,也能通过某些手段进行自动化操作,但是带宽10M的小水管导致每次构建流程运行时间都超过10分钟(多个流程同时运行这个时间甚至会超过30分钟直到超时运行失败);10分钟左右的发布流程时长在平时影响不大,但是在周四的集中发版窗口和紧急线上BUG修复时极大影响工作效率,导致周四加班过长和线上BUG修复缓慢。

解决方案

查阅官方文档后,得知分层Jar镜像特性可以解决上述问题,下面记录下详细的操作步骤。

修改build.gradle

在build.gradle文件中的顶层新增以下内容:

jar {
	// 不需要输出pure jar
    enabled = false
}

bootJar {
    // 不输出version和classifier以便于后续文件定位(最终打包输出jar名称为app.jar)
    archiveBaseName = 'app'
    archiveVersion = ''
    archiveClassifier = ''
    layered {
        enabled = true
        application {
            intoLayer("spring-boot-loader") {
                include "org/springframework/boot/loader/**"
            }
            intoLayer("application")
        }
        dependencies {
            intoLayer("application") {
                includeProjectDependencies()
            }
            intoLayer("snapshot-dependencies") {
                include "*:*:*SNAPSHOT"
            }
            intoLayer("dependencies")
        }
        layerOrder = ["dependencies", "spring-boot-loader", "snapshot-dependencies", "application"]
    }
}

dependencies层为release版本的依赖,spring-boot-loader层为Spring Boot的启动类,snapshot-dependencies层为快照版本的依赖,application层为服务自己的代码。执行BUILD后进入build\libs目录,执行java -Djarmode=layertools -jar app.jar extract命令解压fat jar查看分层效果:
image
可以看到分层后服务中会经常变化的部分已经被分离出来了,下面进行Dockerfile的编写。

修改Dockerfile

使用Docker的分阶段构建特性,实现分层镜像构建:

# 使用openjdk11作为基础镜像
FROM your-repo/openjdk:11-arthas as builder
# 工作目录
WORKDIR /app
# 将fat jar复制到工作目录下
COPY build/libs/app.jar ./app.jar
# 使用分层jar工具解压fat jar
RUN java -Djarmode=layertools -jar app.jar extract

# 使用openjdk11作为基础镜像
FROM your-repo/openjdk:11-arthas
# 工作目录
WORKDIR /app
# Spring Boot生效配置文件环境变量
ENV SPRING_PROFILES_ACTIVE=your-config
# 暴露端口
EXPOSE 8080
# 首先复制上一步分层jar中的普通依赖,该层一般不会变动
COPY --from=builder /app/dependencies/ ./
# 然后复制Spring Boot的启动类
COPY --from=builder /app/spring-boot-loader/ ./
# 接着复制快照版本依赖,该层变动比较频繁
COPY --from=builder /app/snapshot-dependencies/ ./
# 最后复制应用代码,该层变动最为频繁
COPY --from=builder /app/application/ ./
# 暴露启动命令给容器
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

需要注意的是,Spring Boot 3.x版本的启动类变成了org.springframework.boot.loader.launch.JarLauncher。下面是笔者生产环境中的一次发版Jenkins日志镜像推送部分截图:
image
可以看到本次镜像推送复用了大部分镜像层,仅推送了业务代码变动的一层,整体流程耗时降低到了1分钟出头:
image
镜像推送耗时仅不到10秒,相比之前的上传180m左右的fat jar的方案,带宽占用极大降低、构建流程并发极大提升、构建稳定性极大提升。

总结

Spring Boot的分层jar特性能够极大缩短服务部署到k8s中的资源消耗和带宽消耗,有效提升发版效率。不过该种方式也存在部分不足,如release版本依赖变动时耗时会比直接上传fat jar还要长;但是考虑到修改基础依赖的频率较修改业务代码的低很多,这个不足是可以接受的。

posted @ 2025-07-09 09:51  天火33  阅读(24)  评论(0)    收藏  举报