Spring Boot JAR 打包部署实战:从构建命令到 JVM 参数调优

发布时间:2026/10/19 7:43:47
Spring Boot JAR 打包部署实战:从构建命令到 JVM 参数调优
Spring Boot 项目打包成 JAR 并部署运行这件事看着简单实际上从构建到跑起来中间藏着不少坑。我见过很多开发者项目在 IDEA 里跑得稳稳的一打成 jar 包部署到服务器就各种问题启动失败、端口被占、配置文件没生效、内存溢出…… 这篇文章我就把从打包到部署的完整链路拆开揉碎把我踩过坑、验证过可行的实战方案完整分享出来。无论你是刚接触 Spring Boot 的初学者还是正在排查线上部署问题的开发者这篇文章都值得你从头到尾看一遍。1. 打包方案选型为什么大多数项目选择 JAR1.1 JAR 与 WAR 之争到底怎么选很多人在项目启动时期就纠结过到底打成 JAR 还是 WAR这里我直接给出结论如果你的项目不需要部署在外部 Tomcat 上无脑选 JAR。原因有几点第一JAR 包内嵌了 Tomcat或者 Jetty、UndertowJava 环境配好之后一条命令就能跑起来不需要单独安装和配置 Servlet 容器。这对 DevOps 自动化部署非常友好CI/CD 流水线里只需要处理一个 Java 进程。第二JAR 包是自包含的依赖、配置文件、静态资源全部打在一个文件里分发部署的时候特别省心。我见过有些团队因为历史原因还在用 WAR 外部 Tomcat 的方式每次发版都要小心翼翼地处理 Tomcat 的 lib 目录、context 配置出了问题排查成本高很多。第三从微服务和容器化的趋势来看JAR 包结构天然适合构建 Docker 镜像。一个 Spring Boot 应用打成 JAR 后用FROM openjdk:8-jre-alpine加上一条COPY指令和一个ENTRYPOINT镜像就构建出来了非常顺滑。当然如果项目有特殊需求比如必须共享外部 Tomcat 的管理功能、需要在一个容器里跑多个应用那么 WAR 包也有它的价值。但对绝大多数 Spring Boot 单体应用来说JAR 就是最优解。1.2 Maven 插件选型spring-boot-maven-plugin 的不可替代性打成可执行 JAR 的核心工具是spring-boot-maven-plugin插件。这个插件做了三件关键的事它重新组织 JAR 包的内部结构把项目自身的代码放在BOOT-INF/classes/把所有的依赖放在BOOT-INF/lib/把内置容器相关的类放在org/springframework/boot/loader/。它提供一个特殊的JarLauncher作为入口类这个类会创建一个自定义的类加载器能够从BOOT-INF/lib/加载嵌套的 JAR。这就解决了 Java 标准 JAR 无法嵌套加载依赖的问题。它还支持将项目里的依赖全部解压后合并到 JAR 包里fully executable模式方便直接作为系统服务运行。很多初学者有个误区以为 Maven 默认的mvn package就能打出可执行 JAR。实际上如果没配置这个插件打出来的只是普通 JAR运行时跑java -jar会直接报no main manifest attribute。所以项目pom.xml里这段配置是必需的build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build使用 Spring Initializr 生成的项目默认就带上了这个插件但如果你的项目是从旧版本迁移过来的或者手写了 pom.xml一定记得检查这一项。1.3 Gradle 项目怎么办如果你用的是 Gradle思路是一致的只是写法不同。在build.gradle中应用org.springframework.boot插件然后通过bootJar任务生成可执行 JARplugins { id org.springframework.boot version 3.2.5 } bootJar { archiveFileName myapp.jar }执行./gradlew clean bootJar就能生成同样结构的可执行 JAR。本文后续的部署原理对 Maven 和 Gradle 构建出的 JAR 包都适用。2. 打包前的准备那些容易踩坑的配置细节2.1 JDK 版本与编译级别的统一打包后部署环境最常见的“水土不服”大多是 Java 版本不一致导致的。项目在本地用 JDK 17 编译服务器上却只有 JDK 8直接跑必然报UnsupportedClassVersionError。我的建议是在 pom.xml 里显式指定java.version并且尽量让本地开发环境、CI 构建环境和服务器运行时环境保持一致。Maven 会读取这个属性来设置编译参数properties java.version17/java.version /properties如果你用的是 IDEA还可能在 Maven 的compiler插件配置里看到source和target这两个值也要和java.version一致。曾经有个项目就是因为在 IDEA 的 Project Structure 里改了 SDK 版本但 pom.xml 里没改结果本地编译正常CI 构建出的包在服务器上报错。后来我们规定java.version是唯一指定版本的地方IDEA 里的 SDK 设置必须跟随 pom.xml。2.2 确保 main 类能被正确识别spring-boot-maven-plugin会自动查找带有SpringBootApplication注解的类作为启动类。大多数项目只有一个这样的类所以不会有问题。但如果你的项目结构特殊比如有多个模块、或者把启动类放在了一个依赖模块里就需要在插件配置里手动指定plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.MyApplication/mainClass /configuration /plugin顺便说一句启动类的位置要注意。它在默认包java 默认包下时SpringBootApplication的组件扫描范围是当前包及子包所以如果启动类放在根包下它能扫描到所有子包如果启动类放在某个子包下那它无法扫描到它的兄弟包。这个也算常见问题如果发现打包后某些 Bean 找不到先检查启动类的位置对不对。2.3 配置文件与多环境处理Spring Boot 的配置文件支持多环境配置常见的做法是application.yml放公共配置application-dev.yml、application-prod.yml分别放不同环境的配置。打包时这些文件会全部打进 JAR 的BOOT-INF/classes/下。那上线时到底用哪个环境两种方式方式一启动时指定java -jar myapp.jar --spring.profiles.activeprod方式二通过环境变量指定export SPRING_PROFILES_ACTIVEprod java -jar myapp.jar我强烈建议生产环境使用环境变量方式因为环境变量可以由运维统一配置管理不需要改启动脚本。而且它还可以和 Docker、Kubernetes 的配置注入机制自然衔接。这里有个细节需要特别提醒如果你的application.yml里配置了数据库密码、第三方密钥这类敏感信息这些值也会被打进 JAR 包。JAR 包本质上是 ZIP 格式任何人用解压工具就能看到里面的明文配置。所以生产环境的敏感配置一定不要写在项目文件里而是要通过环境变量或外部配置中心在部署时覆盖。2.4 静态资源和第三方依赖的完整性有时打的 JAR 包体积异常小或者启动后页面样式丢失多半是资源文件没有被打包进去。检查src/main/resources目录下的文件是否正确编译到 target/classes 下。如果使用了自定义的 resource 配置注意不要不小心排除掉静态资源目录。依赖方面Maven 打包时默认会尝试从中央仓库解析所有依赖。构建机如果网络受限可能导致部分依赖下载失败最终打出的包缺少必需库。公司内网环境建议配置私服如 Nexus 或 Artifactory并在settings.xml里把私服地址配好。另外特别注意不要使用provided作用域标注运行时必需的依赖否则 JAR 包里不会包含它。3. 完整打包实操从命令到产物的全流程3.1 标准打包命令解析打包命令本身很简单但我建议你理解每一步参数的含义而不是无脑复制mvn clean package -DskipTestsclean删除target目录保证从干净状态开始避免旧产物残留导致的新旧文件混用问题。package执行到 Maven 生命周期中的 package 阶段完成编译、资源复制、JAR 打包。-DskipTests跳过测试执行但会编译测试代码。-Dmaven.test.skiptrue跳过测试代码的编译和执行构建速度更快适合临时打包验证。我第一次用-DskipTests部署时遇到过测试代码编译失败导致整体构建失败的情况后来在赶时间的时候直接改用-Dmaven.test.skiptrue。不过反反复复之后我现在更推荐在打包前先本地跑一遍关键测试省得把低级 bug 带上线。说一下我常用的完整构建命令mvn clean package -Dmaven.test.skiptrue -P prod这里-P prod表示激活 Maven Profile。如果项目里配置了 Profile 来切换不同环境的资源和配置这个参数会生效。注意 Profile 激活机制和 Spring Boot 的spring.profiles.active是两回事Maven Profile 影响的是构建时选择哪些资源文件Spring Boot 的 Profile 影响的是运行时加载哪些配置。3.2 一步到位的 install 与 deploy 区别很多新手会混淆package、install和deploy三个命令mvn package # 打包生成 JAR输出到 target 目录 mvn install # 打包并安装到本地 Maven 仓库 mvn deploy # 打包并上传到远程 Maven 仓库单应用部署用package就够了。install常用在多模块项目中比如你改了公共模块的代码需要先install到本地仓库其他模块才能引用最新版本。deploy则是供团队协作时把构建产物发布到共享仓库。3.3 构建产物的目录结构解读打包完成后在target/目录下会看到两个 JAR 文件比如myapp-1.0.0.jar # 可执行 JAR约 50 MB myapp-1.0.0.jar.original # 原始 JAR约 200 KB刚接触的朋友可能疑惑为什么有两个。.original是 Maven 默认生成的普通 JAR只包含项目自身的代码不能直接运行。spring-boot-maven-plugin在其基础上重新打包生成可执行 JAR。查看文件大小就能轻松分辨。如果你想简化产物命名可以在插件配置里设置configuration finalNamemyapp/finalName /configuration这样生成的 JAR 就叫myapp.jar避免带版本号导致部署脚本还得处理文件名变化的问题。对于持续交付场景一个固定的应用名比版本号更友好版本信息可以记录在 CI 的构建信息里。3.4 使用 Spring Boot 的自动装配检测机制打包后启动时出现奇怪的类加载异常可以开启自动装配报告来排查java -jar myapp.jar --debug这样启动时会在日志里打印CONDITIONS EVALUATION REPORT详细列出哪些自动装配类生效、哪些没生效及其原因。虽然这更多是用在开发阶段但遇到“本地正常、线上启动异常”时打开这份报告往往能快速定位到某个条件装配没满足的问题。4. 部署运行从 java -jar 到服务化管理的进阶之路4.1 最基本的部署准备工作部署前服务器上必须装好 JDK。这里提醒一点Spring Boot 2.x 及以下版本推荐 JDK 8 或 11Spring Boot 3.x 则要求 JDK 17 及以上。去官网下载对应发行版或者使用系统包管理器安装然后验证java -version如果服务器上同时存在多个 JDK 版本设置JAVA_HOME环境变量指向目标版本。启动脚本里最好显式指定 JAVA_HOME避免环境变化导致用了错误的 JDK。把 JAR 包上传到服务器后选择部署目录也有讲究。服务器一般按应用来组织目录如/opt/myapp/日志管理则按业务约定如/var/log/myapp/。不要把 JAR 放在/root或/home下既不规范也容易造成权限问题。顺便把启动命令备份成脚本以start.sh为例#!/bin/bash nohup java -jar /opt/myapp/myapp.jar \ --server.port8080 \ --spring.profiles.activeprod \ /var/log/myapp/app.log 21 这个脚本有几个点要说明nohup让进程忽略挂断信号让进程在后台运行日志输出重定向到文件中。21把标准错误也合并到日志文件避免错误信息丢失。4.2 JVM 参数的合理设置是稳定运行的关键很多线上问题不是代码 bug而是 JVM 参数配置不合理。Spring Boot 应用的堆内存配置我的经验是从这两个参数入手java -Xms512m -Xmx1024m -jar myapp.jar-Xms是初始堆大小-Xmx是最大堆大小。很多人的习惯是只设置-Xmx让初始堆很小然后随着运行逐渐扩容。但实际上频繁扩容和缩容是有性能开销的建议把-Xms和-Xmx设为相同值让 JVM 启动时就分配好内存运行更稳定。堆内存设置多大合适先看服务器总内存预留系统和其他进程所需的空间。比如 4G 内存的服务器JVM 堆设置 2G 到 2.5G 是合理的。不要贪婪地把内存全都分配给 JVM因为还有元空间、线程栈、直接内存等非堆部分需要空间。对于容器化部署JVM 能识别容器的 CPU 和内存限制JDK 8u131 开始支持会自动调整默认堆大小。但在物理机或虚拟机部署时建议还是手动指定便于管理。还有一个对排查问题至关重要的参数-Dfile.encodingUTF-8。Linux 下默认编码可能是POSIX或ANSI_X3.4-1968如果应用中有中文日志或者依赖编码的操作不设置这个可能导致乱码或异常。我踩过一次坑某个报表导出功能在开发环境正常部署到 Linux 后导出的 CSV 文件中文全是乱码罪魁祸首就是环境编码。4.3 前台运行与后台守护进程的正确姿势直接用java -jar myapp.jar会在前台运行终端 CtrlC 后进程就退了。简单部署可以用 nohup 方式但生产环境我更推荐使用 systemd 来托管服务。写一个myapp.service文件放到/etc/systemd/system/[Unit] DescriptionMy Spring Boot Application Afternetwork.target [Service] Usermyapp WorkingDirectory/opt/myapp ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/myapp/myapp.jar SuccessExitStatus143 Restartalways RestartSec10 [Install] WantedBymulti-user.target启用并启动服务systemctl daemon-reload systemctl enable myapp systemctl start myapp这个做法有几个好处服务崩溃了会自动重启Restartalways开机自启日志用journalctl -u myapp -f查看管理命令也统一了。SuccessExitStatus143表示 Java 应用在停止时收到 SIGTERM 信号后退出码为 143要把这个退出码视为正常退出否则 systemd 会以为服务异常停止而重启进程。如果不想用 systemd也可以选 supervisord 这类进程管理工具原理类似。4.4 端口冲突和应用监听地址的坑部署启动时报Port 8080 was already in use是最常见的问题之一。排查与解决方式netstat -tlnp | grep 8080 lsof -i:8080确认哪个进程占用端口后要么杀掉旧进程要么改配置换端口。如果你是在一台机器上跑多个 Spring Boot 应用建议把端口号固定下来并通过server.port配置区分。另外要注意监听地址。Spring Boot 默认监听0.0.0.0也就是所有网卡。如果你只想让内网访问不想对外开放加上server: address: 127.0.0.1或者启动时指定--server.address127.0.0.1。如果明确知道应用只需要本地访问这样配置能减少不必要的暴露面。5. 配置外部化把可变配置从代码中剥离出来5.1 Spring Boot 配置优先级这一点必须搞清楚把配置写在环境变量或外部文件里是为了做到“构建一次到处运行”。Spring Boot 的外部化配置有一个优先级顺序从高到低大致是命令行参数--keyvalueJava 系统属性-Dkeyvalue环境变量SPRING_PROFILES_ACTIVE等当前目录下的config/application.yml当前目录下的application.ymlclasspath 下的config/application.ymlclasspath 下的application.yml这个顺序意味着JAR 包内的application.yml是最低优先级。如果 JAR 包外面有同名配置文件它会覆盖包内配置。利用这个机制你就可以做到包内放默认配置包外放环境差异化配置避免频繁重新打包。实际生产环境推荐的做法是日志路径、熔断阈值、限流规则等经常调整的参数放到外部config/application.yml或通过环境变量注入了。代码库中只保留不敏感的默认值。5.2 一个典型的多环境配置示例假设项目有 dev、test、prod 三套环境JAR 包内包含三份配置文件。启动时通过环境变量选中对应环境而一些敏感参数再通过外部文件或环境变量覆盖export SPRING_PROFILES_ACTIVEprod export DB_PASSWORDxxx java -jar /opt/myapp/myapp.jar对应的application-prod.yml里spring: datasource: url: jdbc:mysql://prod-db-host:3306/myapp username: myapp password: ${DB_PASSWORD}这种方案的巧妙之处在于数据库地址这类相对固定的信息写在配置文件里而密码这种敏感信息通过环境变量注入两边解耦互不污染。5.3 使用 Spring Cloud Config 或 Nacos 的场景当服务数量多、配置中心化管理成为硬需求时可以考虑引入配置中心。但这不是必须的。我见过有的团队四五个服务用环境变量和外部配置文件就已经管理得很好了也见过几十个服务的团队如果没有配置中心每次改配置都要重新发版后来还是上了配置中心。引入配置中心有一个明显的前置成本服务启动时如果配置中心不可用应用可能启动失败同时团队需要对配置变更的发布流程制定规范。如果你只有一两个 Spring Boot 服务真的没必要为了“技术先进”而引入配置中心。6. 常见问题与排查技巧实录6.1 问题速查表下面这些是我在实际部署和帮其他团队排障时经常遇到的问题整理成了一张速查表现象可能原因解决方案启动即报 ClassNotFoundExceptionJAR 包损坏或依赖缺失重新执行 mvn clean package检查本地仓库完整性端口被占用同机多应用冲突修改 server.port或 lsof 定位后清理进程内存溢出 OOM堆内存太小或内存泄漏调大 -Xmx生成 heap dump 分析日志中文乱码文件编码不一致启动时指定 -Dfile.encodingUTF-8外部配置文件不生效配置目录位置不对检查是否放在当前目录的 config/ 子目录下时间不对时区设置错误启动参数加 -Duser.timezoneAsia/Shanghai上传的 JAR 跑不起来用了普通 JAR 而非可执行 JAR检查是否有 .original 文件对比体积6.2 一次 OOM 问题的排查过程实录有一次运维反馈生产环境某个报表服务每隔几天就挂一次日志里出现了java.lang.OutOfMemoryError: Java heap space。当时我们的第一反应是把-Xmx调大从 1G 调到 2G。结果调完之后服务从三天一挂变成五天一挂根本问题没有解决。后来通过两个手段定位到真凶加上-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/var/log/myapp/heapdump.hprof参数让 JVM 在内存溢出时自动导出堆快照用 JProfiler 工具分析堆快照发现大量报表查询的查询结果保存在了一个静态 List 里。这是因为记报表导出功能的缓存没有设置容量上限。找到问题后改成使用带过期策略的缓存并限制结果集大小问题彻底解决。这个案例想说明的是遇到 OOM 不要只想到调内存先分析内存里存着哪些对象往往能定位到真正的代码问题。排查 OOM 的基本流程是先收集现场heap dump、GC 日志再离线分析最后修改代码。6.3 启动慢、日志不输出怎么办Spring Boot 应用启动慢排查思路不难。先确认日志是否配置了输出spring-boot-starter-web默认只输出到 console如果没有配置日志文件路径nohup重定向的日志里内容会很少。建议在配置文件中设置logging: file: name: /var/log/myapp/app.log logback: rollingpolicy: max-file-size: 100MB max-history: 30日志文件切分和保留策略对于线上问题排查特别重要。日志无限增长会把磁盘写满切分策略定好之后运维也不用频繁清日志了。如果启动过程非常慢检查是否依赖了启动时无法连接的外部服务。例如某个配置类用PostConstruct方法去连接 RedisRedis 域名在服务器上解析不了每次启动都要等超时之后才继续。这种问题通常要把外部依赖的连接动作改成懒加载或加超时控制。6.4 升级 Spring Boot 版本后的部署注意点版本升级是另一个常见的坑源。比如从 2.x 系列升级到 3.x 系列时包名从javax.*迁移到了jakarta.*代码里的import javax.servlet.*全部要改。这类升级往往伴随大量配置和依赖的调整部署前务必仔细比对官方文档中的迁移指南。遇到版本升级的时候不要只看新功能重点看 Breaking Changes 列表。我就是从前人踩坑中总结了教训先在一个专门的分支上升级把测试跑通再合入主干部署时尽量保留旧版本 JAR 作为回滚选项这样如果线上出现问题可以第一时间回滚到旧版本。7. 一些实用的部署与运维技巧7.1 优雅停机避免请求被强制中断线上调整或发布时需要保证服务停止时正在处理的请求能正常完成。Spring Boot 2.3 支持优雅停机spring: lifecycle: timeout-per-shutdown-phase: 30s配合 systemd 的Restartalways可以实现服务更新时先停掉接入流量再等待在途请求处理完毕最后退出进程。7.2 健康检查与自动恢复给应用加上spring-boot-starter-actuator依赖后可以暴露健康检查端点dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency配置management: endpoints: web: exposure: include: health,info endpoint: health: show-details: always这样部署后可以直接访问/actuator/health来确认服务状态返回内容是{status:UP}配合监控系统做告警服务状态一清二楚。监控和健康检查在基础设施层面做好服务才能算真正意义上的“部署完成”。7.3 维护本机 Maven 仓库和依赖缓存的几个建议写小工具、开发本地测试时可以用mvn dependency:tree查看依赖关系用mvn dependency:purge-local-repository清理 Maven 本地仓库中的依赖缓存。好几个奇奇怪怪的构建问题都是本地仓库缓存了坏掉的依赖导致的。另外在公司内网环境下如果发现某个依赖在 CI 和本地构建结果不一致先检查是不是 Maven 私服的仓库策略出了问题。之前有个团队就是一再用错私服上的旧版本快照依赖导致本地构建成功、CI 构建失败查了好久才发现是私服仓库的 release 策略覆盖了快照版本。8. 从亲手打包开始深入理解你的项目写到这里整个从打包到部署的流程已经拉通了一遍。从 JAR 和 WAR 的选择、插件原理、打包命令的参数含义到多环境配置、外部化配置、JVM 参数、systemd 托管一直到常见的线上排查案例每块内容都是我实际碰过、验证过的方案。我个人在实际操作中的体会是打包部署这件事要真正做顺离不开对两个层面的理解。第一层是构建工具和插件你要清楚mvn package之后到底生成了什么spring-boot-maven-plugin起了什么作用这样遇到产物不对、缺依赖这类问题才不会懵。第二层是运行环境和配置管理JVM 参数、环境变量、配置优先级这些决定了你的 JAR 包在服务器上是不是能稳定健康地跑起来。最后再分享一个小技巧不管项目多简单都建议提交时保留一份部署文档把启动命令、JDK 版本、JVM 参数、日志路径、健康检查地址都记清楚。等到某一天服务器需要迁移、或者同事需要接手部署时你会感激当初那个认真记文档的自己。