把一个简单的 Spring Boot 项目打成可执行 Jar,体积轻松超过 50MB,如果再引入一些数据源驱动、缓存客户端、消息中间件的 SDK,冲到 150MB 以上也很常见。而真正属于你自己写的业务代码,通常只有几兆。剩下的体积全是依赖库、内嵌 Tomcat、资源文件这些东西。体积大带来的问题不只是占磁盘,更麻烦的是每次发布都要传输这个大文件,内网上传慢、容器镜像推送拉取慢、多机部署时同步慢,这些都会拖累整个交付流程。这篇文章就来系统梳理一下 Jar 包为什么大,以及几种经过实践验证的瘦身方案。

先搞清楚 Jar 包为什么这么大
瘦身之前得先知道体积都被什么吃掉了。Spring Boot 的可执行 Jar 采用嵌套式结构,BOOT-INF/lib 目录下存放了所有依赖的第三方库,BOOT-INF/classes 下是业务代码,org/springframework/boot/loader 下是引导加载器。绝大多数情况下,体积的大头都在 BOOT-INF/lib 里。
可以直接用压缩软件打开 Jar 查看,也可以用命令解压分析。下面这个命令可以列出 Jar 内所有文件并按大小排序:
# 解压后统计 lib 目录下依赖的大小,按体积倒序 unzip -o app.jar -d app-output du -sh app-output/BOOT-INF/lib/* | sort -rh | head -20
常见的大户包括:各种日志实现(logback、log4j2 相关 jar)、内嵌 Tomcat、数据库驱动(MySQL、Oracle 的驱动都不小)、云厂商 SDK(OSS、SLS 这类 SDK 经常带一堆传递依赖)、以及一些被全量引入但只用了一小部分功能的组件,比如引入了 spring-cloud-starter 全家桶却只用了其中一两个功能。分析清楚构成之后,第一步应该做的是清理无用依赖:检查 pom.xml 中那些为了图方便引入的宽泛依赖,换成精确的子模块依赖;对于 scope 为 compile 但只在编译期需要的库,改成 provided。这一步往往不花多少力气就能减掉几十兆。
方案一:分层打包配合 Docker 镜像缓存
Spring Boot 2.3 之后官方支持分层打包(Layered Jar),它不减少 Jar 本身的体积,但能大幅减少每次实际传输的数据量。原理是把 Jar 内容按照变化频率拆成多个层:依赖库基本不变放一层,业务代码频繁变化单独一层。构建 Docker 镜像时,各层分别作为镜像层,只要依赖没变,这些层在镜像仓库中就直接复用缓存,推送和拉取时只传输变化的业务代码那一层。
开启分层打包只需要两步。第一步在 pom.xml 中配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
第二步在 Dockerfile 中使用 unpack 模式提取各层:
FROM eclipse-temurin:17-jre as builder WORKDIR application COPY target/app.jar app.jar # 按层解压,提取顺序即依赖层到应用层 RUN java -Djarmode=layertools -jar app.jar extract FROM eclipse-temurin:17-jre WORKDIR application # 依赖层几乎不变,充分利用镜像缓存 COPY --from=builder application/dependencies/ ./ COPY --from=builder application/spring-boot-loader/ ./ COPY --from=builder application/snapshot-dependencies/ ./ COPY --from=builder application/application/ ./ ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
这个方案的优点是官方原生支持、无侵入、效果稳定,配合镜像仓库的缓存机制,日常发版真正传输的数据量可能只有几兆。缺点是它只适用于容器化部署,如果你还是传统的虚拟机部署方式,分层打包帮不上忙。另外要注意自定义分层顺序,如果项目里有经常变化的本地依赖,应该把它放到 application 层附近,避免破坏缓存。
方案二:依赖外置,业务 Jar 单独更新
传统服务器部署场景下更实用的做法是把依赖库从业务 Jar 中剥离出来。打包时只把第三方依赖放到一个独立的 lib 目录,业务代码单独打成一个轻量 Jar,启动时通过 classpath 指定依赖路径。这样每次发版只需要更新几百 KB 到几 MB 的业务 Jar,依赖目录除非有变化否则长期不动。
在 Maven 中配置依赖拷贝和精简打包:
<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>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
启动命令改为指定 classpath:
java -Dloader.path=lib -cp app.jar org.springframework.boot.loader.PropertiesLauncher
这里用了 PropertiesLauncher 作为启动器,它支持通过 loader.path 加载外部目录中的 Jar。注意使用这种方式时,Jar 仍需用 Spring Boot 插件打包,但业务代码和依赖分离后,上传体积骤减。这个方案适合服务器直部署、发布频率高的项目。缺点是需要维护两份产物的版本一致性,如果依赖目录和业务 Jar 版本不匹配,可能出现运行时找不到类的诡异问题,建议在部署脚本中加入依赖目录的版本校验,或者依赖变更时全量发布一次。
方案三:thin launcher 与启动时按需拉取依赖
还有一种更激进的思路:业务 Jar 里完全不包含依赖,首次启动时根据 pom 信息自动从 Maven 仓库下载依赖到本地缓存。这就是 spring-boot-thin-launcher 插件的工作方式。打包产物通常只有几 MB,甚至几百 KB。
<plugin>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-maven-plugin</artifactId>
<version>1.0.30.RELEASE</version>
<executions>
<execution>
<id>thin-packge</id>
<goals>
<goal>resolve</goal>
<goals>
</execution>
</executions>
</plugin>
首次启动会触发依赖下载,之后依赖缓存在本地 ~/.m2/thin/repo 目录,后续启动速度正常。这个方案的明显短板是对网络环境有强依赖,目标服务器必须能访问 Maven 仓库(或内部私服),而且首次部署时间较长。在内网管控严格、无法直连仓库的环境里要谨慎使用。另外该插件属于 experimental 项目,更新不算活跃,生产环境采用前建议充分验证。
其他补充手段与方案选型建议
除了上面三种主流方案,还有一些辅助手段值得一提。一是压缩比优化,Spring Boot 打包时可排除不必要的资源,比如文档、源码 Jar、测试资源;二是用 spring-boot-maven-plugin 的 requiresUnpack 处理某些必须解压才能加载的库;三是考虑换用 GraalVM Native Image,产物体积和启动速度都有质的飞跃,但改造成本高,反射、动态代理相关的问题需要逐一适配,适合新项目或对启动速度极端敏感的场景。
选型上可以参考这样的组合:容器化部署的项目,首选 layer 分层打包,配置成本最低收益最大;传统虚拟机部署、发版频繁的项目,采用依赖外置方案,配合发布脚本管理依赖目录;边缘节点多、带宽极差但可控的场景,可以考虑 thin launcher。无论选哪种,第一步永远是把 pom 里的无用依赖清干净,这是零成本且必须做的事。瘦身不是一刀切,要结合部署架构、团队运维习惯和回滚需求综合决定,切莫为了追求小体积牺牲了部署的可追溯性和回滚的便捷性。
Spring Boot瘦身Jar包体积优化分层打包修改时间:2026-09-06 10:46:42