Spring Boot 应用部署优化:打包体积缩减 80% 实战
介绍 Spring Boot 应用打包体积优化方案。通过依赖精简(排除传递依赖、替换重型库)、分层 JAR 构建(配合 Docker 缓存)、自定义类加载及 GraalVM Native Image 等技术手段,将典型应用从 150MB 缩减至 30MB。同时提供 Maven 配置、Dockerfile 多阶段构建及 CI/CD 集成示例,实现镜像体积减少 73%,启动时间缩短 73%,兼顾性能与可维护性。

介绍 Spring Boot 应用打包体积优化方案。通过依赖精简(排除传递依赖、替换重型库)、分层 JAR 构建(配合 Docker 缓存)、自定义类加载及 GraalVM Native Image 等技术手段,将典型应用从 150MB 缩减至 30MB。同时提供 Maven 配置、Dockerfile 多阶段构建及 CI/CD 集成示例,实现镜像体积减少 73%,启动时间缩短 73%,兼顾性能与可维护性。

假设我们有一个标准的 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% | 97.5MB |
| Spring Framework | 20% | 30MB |
| 业务代码 | 2% | 3MB |
| 嵌入式 Tomcat | 10% | 15MB |
| 其他资源 | 3% | 4.5MB |
分析结论:
首先,我们需要找出哪些依赖占用空间最大。
# 查看依赖树
mvn dependency:tree
# 分析依赖大小
mvn com.github.ferstl:depgraph-maven-plugin:aggregate
# 使用 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 | ~20MB | Excel 处理全套组件 |
itextpdf | ~18MB | PDF 生成库 |
tensorflow-core | ~50MB | AI 模型(如有) |
guava | ~8MB | Google 工具库 |
| 方案 | 预期减少 | 目标体积 |
|---|---|---|
| 移除未使用依赖 | 20-30MB | 120MB |
| 模块化拆分 | 40-50MB | 100MB |
| 启用分层构建 | 60-70MB | 50MB |
| 使用 Native Image | 80-100MB | 30MB |
<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>
<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>
// 场景:Excel 导出功能
// 方案 A:使用 Apache POI(重型,~20MB)
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.*;
// 方案 B:使用 EasyExcel(轻量,~2MB)
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.WriteSheet;
// 代码对比
public class ExcelExportExample {
// EasyExcel 实现(内存占用更小)
public void exportLightweight(HttpServletResponse response) {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
EasyExcel.write(response.getOutputStream(), UserData.class)
.sheet("用户数据")
.doFinish(() -> {
// 流式写入,内存友好
});
}
}
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>
| Layer | 内容 | 变化频率 |
|---|---|---|
| Layer 1 | 依赖 | 极低 |
| Layer 2 | Spring Boot Loader | 中等 |
| Layer 3 | SNAPSHOT 依赖 | 高 |
| Layer 4 | 应用代码 | 高 |
| Layer 5 | 应用资源 | 高 |
优化原理:Docker 构建时,只有变化的层会被重新构建和推送,变化频率低的层可以长期复用缓存。
# 构建阶段
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"]
/**
* 外部依赖加载器
* 将依赖放在外部 lib/ 目录,减少主 JAR 体积
*/
public class ExternalDependencyLauncher {
public static void main(String[] args) throws Exception {
File libDir = new File("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) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
URLClassLoader classLoader = new URLClassLoader(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);
}
}
<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>
| 特性 | 传统 JVM | Native Image |
|---|---|---|
| 启动时间 | ~2-5 秒 | ~0.1 秒 |
| 类加载 | ~1-3 秒 | 直接执行 |
| JIT 编译 | 运行时 | 编译期完成 |
<!-- 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>
// 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}
]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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>
# 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.yml
spring:
# 禁用不需要的自动配置
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
# Web 优化
web:
resources:
chain:
cache: true
static-locations: classpath:/static/
server:
# Undertow 配置
undertow:
threads:
io: 4
worker: 20
buffer-size: 1024
direct-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 MB | 30 MB | ⬇️ 80% |
| Docker 镜像 | 450 MB | 120 MB | ⬇️ 73% |
| 启动时间 | 8.5 秒 | 2.3 秒 | ⬇️ 73% |
| 内存占用 (运行时) | 512 MB | 256 MB | ⬇️ 50% |
| 构建时间 | 90 秒 | 120 秒 | ⬆️ 33% |
注意:构建时间略有增加是正常的,因为增加了分层构建和优化步骤,但这是一次性成本。
| 应用类型 | 选择优化方案 | 体积减少 | 适用场景 |
|---|---|---|---|
| 传统 Web 应用 | 方案 1: 依赖精简 + 分层构建 | 50% | 传统部署、虚拟机 |
| 微服务 | 方案 2: 多阶段构建 + 外部依赖 | 60% | Kubernetes、容器化 |
| Serverless | 方案 3: Native Image | 80% | Serverless、边缘计算 |
# 定期检查依赖大小
mvn dependency:tree | grep -E "compile|runtime"
# 使用 Maven 插件分析
mvn com.github.ferstl:depgraph-maven-plugin:aggregate
<!-- 在父 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>
# .github/workflows/docker-build.yml
name: 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: true
tags: user/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 过度优化 | 为了优化而优化,牺牲可维护性 | 明确优化目标,权衡利弊 |
| 忽略测试 | 删除'未使用'的依赖后测试失败 | 完善测试覆盖,使用依赖分析工具 |
| Native Image 兼容性 | 反射、动态代理可能不工作 | 提前测试,编写配置文件 |
| 安全隐患 | 使用精简镜像可能缺少安全补丁 | 定期更新基础镜像 |

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online