Spring Boot 部署优化:打包体积缩小 80% 的秘诀

Spring Boot 部署优化:打包体积缩小 80% 的秘诀
在这里插入图片描述

✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

在这里插入图片描述
在微服务架构盛行的今天,Spring Boot 应用的打包体积直接影响着部署效率和资源成本。本文将分享如何通过一系列优化手段,将一个典型 Spring Boot 应用的打包体积从 150MB 缩减至 30MB,缩减幅度达 80%。

目录


问题背景

典型场景

假设我们有一个标准的 Spring Boot Web 应用,包含以下依赖:

# 项目依赖概览dependencies:- spring-boot-starter-web - spring-boot-starter-data-jpa - spring-boot-starter-security - spring-boot-starter-validation - mysql-connector-java - lombok - commons-lang3 - guava - poi (Excel处理) - itext (PDF处理) 

执行 mvn clean package 后,得到一个 150MB 的 fat JAR:

target/ ├── application-1.0.0.jar (150MB) └── original-application-1.0.0.jar (30KB) 

为什么这么大?

让我们深入分析这个 150MB 的 JAR 包结构:

65%20%10%3%2%Spring Boot Fat JAR 体积构成分析第三方依赖库 [65]Spring Framework [20]业务代码 [2]嵌入式Tomcat [10]其他资源 [3]

分析结论

  • 第三方依赖 占据了 65% 的体积(97.5MB)
  • Spring Framework 自身占用 20%(30MB)
  • 业务代码 仅占 2%(3MB)
  • 嵌入式 Tomcat 占用 10%(15MB)

体积分析

1. 依赖分析工具

首先,我们需要找出哪些依赖占用空间最大。

Maven Dependency Analyzer
# 查看依赖树 mvn dependency:tree # 分析依赖大小 mvn com.github.ferstl:depgraph-maven-plugin:aggregate 
JAR 分析工具
# 使用 jar 命令查看内容 jar tf application.jar |head -20 # 解压后分析目录大小unzip -q application.jar -d app-extracted du -sh app-extracted/BOOT-INF/lib/* |sort -hr |head -10 
典型的大体积依赖
依赖体积说明
spring-boot-starter-web~15MB包含完整的 Web 模块
poi-ooxml~20MBExcel 处理全套组件
itextpdf~18MBPDF 生成库
tensorflow-core~50MBAI 模型(如有)
guava~8MBGoogle 工具库

2. 瘦身机会识别

Fat JAR 150MB

分析优化机会

移除未使用依赖
预期减少: 20-30MB

模块化拆分
预期减少: 40-50MB

启用分层构建
预期减少: 60-70MB

使用 Native Image
预期减少: 80-100MB

目标: 120MB

目标: 100MB

目标: 50MB

目标: 30MB


优化策略

策略一:依赖精简

1.1 排除传递依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><!-- 排除默认的 Tomcat,改用 Undertow --><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><!-- Undertow 更轻量 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency></dependencies>
1.2 使用范围限定依赖
<dependencies><!-- 测试依赖仅在测试时使用 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!-- Lombok 仅在编译时需要 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></dependencies>
1.3 替换重型依赖
// 场景:Excel 导出功能// 方案 A:使用 Apache POI(重型,~20MB)importorg.apache.poi.ss.usermodel.*;importorg.apache.poi.xssf.usermodel.*;// 方案 B:使用 EasyExcel(轻量,~2MB)importcom.alibaba.excel.EasyExcel;importcom.alibaba.excel.write.metadata.WriteSheet;// 代码对比publicclassExcelExportExample{// EasyExcel 实现(内存占用更小)publicvoidexportLightweight(HttpServletResponse response){ response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8");EasyExcel.write(response.getOutputStream(),UserData.class).sheet("用户数据").doFinish(()->{// 流式写入,内存友好});}}

策略二:分层 JAR 构建

2.1 Spring Boot Layered JARs

Spring Boot 2.3+ 支持分层 JAR,配合 Docker 可以极大优化镜像大小。

<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layers><enabled>true</enabled></layers></configuration></plugin></plugins></build>
2.2 分层结构说明

Layered JAR

Layer 1: 依赖

Layer 2: Spring Boot Loader

Layer 3: SNAPSHOT 依赖

Layer 4: 应用代码

Layer 5: 应用资源

变化频率: 极低

变化频率: 中等

变化频率: 高

优化原理:Docker 构建时,只有变化的层会被重新构建和推送,变化频率低的层可以长期复用缓存。

2.3 多阶段 Dockerfile
# 构建阶段 FROM maven:3.8-openjdk-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 运行阶段 - 使用精简的基础镜像 FROM eclipse-temurin:17-jre-alpine WORKDIR /app # 只复制必要的文件 COPY --from=builder /app/target/*.jar app.jar # 创建非 root 用户 RUN addgroup -S spring && adduser -S spring -G spring USER spring:spring EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] 

策略三:自定义类加载

3.1 外部依赖 JAR
/** * 外部依赖加载器 * 将依赖放在外部 lib/ 目录,减少主 JAR 体积 */publicclassExternalDependencyLauncher{publicstaticvoidmain(String[] args)throwsException{File libDir =newFile("lib"); URL[] jarUrls =Arrays.stream(Objects.requireNonNull(libDir.listFiles())).filter(file -> file.getName().endsWith(".jar")).map(File::toURI).map(uri ->{try{return uri.toURL();}catch(MalformedURLException e){thrownewRuntimeException(e);}}).toArray(URL[]::new);URLClassLoader classLoader =newURLClassLoader(jarUrls);Thread.currentThread().setContextClassLoader(classLoader);// 启动 Spring Boot 应用Class<?> appClass = classLoader.loadClass("com.example.Application");Method mainMethod = appClass.getMethod("main",String[].class); mainMethod.invoke(null,(Object) args);}}
3.2 Maven 配置
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifest><mainClass>com.example.ExternalDependencyLauncher</mainClass><classpathPrefix>lib/</classpathPrefix><addClasspath>true</addClasspath></manifest></archive></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>copy-dependencies</id><phase>package</phase><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/lib</outputDirectory></configuration></execution></executions></plugin></plugins></build>

策略四:GraalVM Native Image

4.1 原理说明

Java 源码

Maven 编译

Class 文件

GraalVM Native Image

原生可执行文件

传统 JVM

JVM 启动
~2-5秒

类加载
~1-3秒

JIT 编译
运行时

应用运行

Native Image

直接执行
~0.1秒

4.2 配置示例
<!-- pom.xml --><dependencies><dependency><groupId>org.springframework.experimental</groupId><artifactId>spring-native</artifactId><version>0.12.0</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.experimental</groupId><artifactId>spring-aot-maven-plugin</artifactId><version>0.12.0</version><executions><execution><id>test-generate</id><goals><goal>test-generate</goal></goals></execution><execution><id>generate</id><goals><goal>generate</goal></goals></execution></executions></plugin></plugins></build><profiles><profile><id>native</id><build><plugins><plugin><groupId>org.graalvm.buildtools</groupId><artifactId>native-maven-plugin</artifactId><version>0.9.13</version><executions><execution><id>build-native</id><goals><goal>build</goal></goals><phase>package</phase></execution></executions></plugin></plugins></build></profile></profiles>
4.3 反射配置
// src/main/resources/META-INF/native-image/reflect-config.json[{"name":"com.example.model.User","allDeclaredConstructors":true,"allPublicConstructors":true,"allDeclaredMethods":true,"allPublicMethods":true,"allDeclaredFields":true,"allPublicFields":true},{"name":"com.example.controller.UserController","allDeclaredConstructors":true,"allDeclaredMethods":true}]

实战演示

完整优化配置

Maven 配置文件
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><groupId>com.example</groupId><artifactId>optimized-spring-boot</artifactId><version>1.0.0</version><name>Optimized Spring Boot Application</name><properties><java.version>17</java.version><!-- 排除不必要的依赖 --><spring-boot-admin.version>3.1.0</spring-boot-admin.version></properties><dependencies><!-- Web 模块 - 使用 Undertow 替代 Tomcat --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-undertow</artifactId></dependency><!-- 只引入需要的模块 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- 数据库 - 按需引入 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- 驱动 - 使用 runtime scope --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- 工具类 - 选择轻量级替代品 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><!-- Lombok - provided scope --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency><!-- 测试依赖 - test scope --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><!-- Spring Boot 分层插件 --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layers><enabled>true</enabled></layers><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin><!-- 依赖分析插件 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>analyze</id><goals><goal>analyze</goal></goals></execution></executions></plugin></plugins></build></project>
优化后的 Dockerfile
# syntax=docker/dockerfile:1.4 # 多阶段构建 + BuildKit 缓存优化 # =================================== # 阶段 1: 依赖解析 (利用 Docker 缓存) # =================================== FROM maven:3.8-eclipse-temurin-17 AS deps WORKDIR /app # 先复制 pom.xml,下载依赖(这一层会被缓存) COPY pom.xml ./ RUN mvn dependency:go-offline -B # =================================== # 阶段 2: 编译 (代码变化时重新执行) # =================================== FROM maven:3.8-eclipse-temurin-17 AS builder WORKDIR /app # 从 deps 阶段复制本地仓库 COPY --from=deps /root/.m2 /root/.m2 # 复制源代码 COPY src ./src # 构建应用 RUN mvn clean package -DskipTests -B && \ mv target/*.jar app.jar # =================================== # 阶段 3: 运行时镜像 (最小化体积) # =================================== FROM eclipse-temurin:17-jre-alpine # 安装必要的工具和时区数据 RUN apk add --no-cache tzdata && \ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ echo "Asia/Shanghai" > /etc/timezone && \ apk del tzdata WORKDIR /app # 创建非特权用户 RUN addgroup -S appgroup && \ adduser -S appuser -G appgroup # 复制应用 JAR COPY --from=builder --chown=appuser:appgroup /app/app.jar app.jar # 切换用户 USER appuser # JVM 参数优化 ENV JAVA_OPTS="-XX:+UseContainerSupport \ -XX:MaxRAMPercentage=75.0 \ -XX:InitialRAMPercentage=50.0 \ -XX:+UseG1GC \ -Djava.security.egd=file:/dev/./urandom" EXPOSE 8080 # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] 
应用配置优化
# application.ymlspring:# 禁用不需要的自动配置autoconfigure:exclude:- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration # Web 优化web:resources:chain:cache:truestatic-locations: classpath:/static/ server:# Undertow 配置undertow:threads:io:4worker:20buffer-size:1024direct-buffers:true# 管理端点management:endpoints:web:exposure:include: health,info,metrics endpoint:health:show-details: always # 日志配置logging:level:root: INFO com.example: DEBUG pattern:console:"%d{yyyy-MM-dd HH:mm:ss} - %msg%n"

效果对比

优化前后对比表

指标优化前优化后改善幅度
JAR 包大小150 MB30 MB⬇️ 80%
Docker 镜像450 MB120 MB⬇️ 73%
启动时间8.5 秒2.3 秒⬇️ 73%
内存占用(运行时)512 MB256 MB⬇️ 50%
构建时间90 秒120 秒⬆️ 33%
注意:构建时间略有增加是正常的,因为增加了分层构建和优化步骤,但这是一次性成本。

体积优化分解图

35%25%20%10%10%各优化策略贡献分析依赖精简分层构建Undertow替换Native编译其他优化

不同方案适用场景

传统Web应用

微服务

云原生

选择优化方案

应用类型

方案1: 依赖精简
+ 分层构建
体积减少: 50%

方案2: 多阶段构建
+ 外部依赖
体积减少: 60%

方案3: Native Image
体积减少: 80%
启动时间减少: 90%

适用场景:
传统部署、虚拟机

适用场景:
Kubernetes、容器化

适用场景:
Serverless、边缘计算


最佳实践

1. 持续监控

# 定期检查依赖大小 mvn dependency:tree |grep -E "compile|runtime"# 使用 Maven 插件分析 mvn com.github.ferstl:depgraph-maven-plugin:aggregate 

2. 依赖管理规范

<!-- 在父 POM 中统一管理版本 --><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>3.2.0</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

3. CI/CD 集成

# .github/workflows/docker-build.ymlname: Build and Push Docker Image on:push:branches:[main]jobs:build:runs-on: ubuntu-latest steps:-uses: actions/checkout@v3 -name: Set up JDK 17 uses: actions/setup-java@v3 with:java-version:'17'distribution:'temurin'-name: Build with Maven run: mvn clean package -DskipTests -name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 -name: Login to Docker Hub uses: docker/login-action@v2 with:username: ${{ secrets.DOCKER_USERNAME }}password: ${{ secrets.DOCKER_PASSWORD }}-name: Build and push uses: docker/build-push-action@v4 with:context: . push:truetags: user/app:latest cache-from: type=gha cache-to: type=gha,mode=max build-args:| BUILDKIT_INLINE_CACHE=1

4. 避免的陷阱

陷阱说明解决方案
过度优化为了优化而优化,牺牲可维护性明确优化目标,权衡利弊
忽略测试删除"未使用"的依赖后测试失败完善测试覆盖,使用依赖分析工具
Native Image 兼容性反射、动态代理可能不工作提前测试,编写配置文件
安全隐患使用精简镜像可能缺少安全补丁定期更新基础镜像

总结

优化路线图

开始
Fat JAR 150MB

第一步
依赖分析

第二步
依赖精简
120MB

第三步
切换Undertow
110MB

第四步
分层构建
50MB

是否需要
极致优化?

第五步
Native Image
30MB

完成
50MB

完成
30MB

关键要点

  1. 先分析再优化:使用工具找出真正的体积大头
  2. 分步优化:不要一次性应用所有策略,逐步验证
  3. 权衡取舍:体积、性能、兼容性三者之间找平衡
  4. 团队协作:建立依赖管理规范,防止体积膨胀
  5. 持续改进:定期审查依赖,移除不再使用的库

参考资料


✍️ 坚持用清晰易懂的图解+可落地的代码,让每个知识点都简单直观!💡 座右铭:“道路是曲折的,前途是光明的!”

Read more

【算法通关指南:算法基础篇】二分答案专题:1.木材加工 2.砍树

【算法通关指南:算法基础篇】二分答案专题:1.木材加工 2.砍树

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南 》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、二分答案 * 二、二分答案经典算题 * 2.1 木材加工 * 2.1.1题目 * 2.1.2 算法原理 * 2.1.3 代码 * 2.2 砍树 * 2.2.1 题目 * 2.2.2 算法原理 * 2.2.3 代码 * 总结与每日励志 前言 二分答案是算法竞赛与笔试中极具技巧性的高分解法,核心思路是将复杂求解转化为简洁的二分+判定,

By Ne0inhk
160. 相交链表 - 题解与详细分析

160. 相交链表 - 题解与详细分析

题目描述 给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null。 图示两个链表在节点 c1 开始相交: text A: a1 → a2 ↘ c1 → c2 → c3 ↗ B: b1 → b2 → b3 注意: * 题目数据保证整个链式结构中不存在环 * 函数返回结果后,链表必须保持其原始结构 解题思路 这道题要求找到两个链表的相交节点。由于链表可能长度不同,但相交后的部分长度相同,我们可以通过以下方法解决: 核心思想 1. 计算链表长度:分别遍历两个链表,得到它们的长度 2. 对齐起点:让长链表先移动长度差值的步数,使两个链表的剩余长度相等 3. 同时遍历:两个指针同时移动,比较节点是否相同 为什么这样有效? * 如果两个链表相交,那么从相交点到链表末尾的长度是相同的 * 通过让长链表先走差值步,两个指针会同时到达相交点(

By Ne0inhk
动态规划 线性 DP 经典四题一遍吃透

动态规划 线性 DP 经典四题一遍吃透

文章目录 * 台阶问题 * 最大子段和 * 传球游戏 * 乌龟棋 线性dp 是动态规划问题中最基础、最常⻅的⼀类问题。它的特点是状态转移只依赖于前⼀个或前⼏个状态,状态之间的关系是线性的,通常可以⽤⼀维或者⼆维数组来存储状态。 我们在⼊⻔阶段解决的《下楼梯》以及《数字三⻆形》其实都是线性dp,⼀个是⼀维的,另⼀个是⼆ 维的。 台阶问题 题目描述 题目解析 本题就是上一节下楼梯的问题的加强版,总体思路不变,下面我们还是按照动规5板斧来分析一下这道题。 1、状态表示 dp[i]表示走到第i个台阶的所有方案数 2、状态转移方程 第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和,因为本题数据比较大,用long long都无法保证数据不越界,所以题目规定方案数还需要模100003,第i个台阶的方案数等于从i-1阶到i-k阶的所有方案数之和再模上100003,所以但是注意是可能越界访问的,比如i为3,

By Ne0inhk
国内python职位数据分析_flask+spider

国内python职位数据分析_flask+spider

1. 开发语言:Python 2. 框架:flask 3. Python版本:python3.8 4. 数据库:mysql 5.7 5. 数据库工具:Navicat12 6. 开发软件:PyCharm 系统展示 系统首页 招聘信息页面 管理员登录 管理员功能界面 用户管理 招聘信息管理 看板展示 论坛交流 系统管理 摘要 系统阐述的是使用国内python职位数据分析系统的设计与实现,对于Python、B/S结构、MySql进行了较为深入的学习与应用。主要针对系统的设计,描述,实现和分析与测试方面来表明开发的过程。开发中使用了 Flask框架和MySql数据库技术搭建系统的整体架构。利用这些技术结合实际需求开发了具有个人中心、用户管理、招聘信息管理、论坛交流、系统管理等功能的系统,最后对系统进行相应的测试,测试系统有无存在问题以及测试用户权限来优化系统,最后系统达到预期目标。

By Ne0inhk