SpringBoot打包部署:jar与war包原理、配置与实战避坑指南

发布时间:2026/10/28 15:51:28
SpringBoot打包部署:jar与war包原理、配置与实战避坑指南 做后端开发这些年打包部署这件事看着不起眼但真到了上线那一刻打包方式选错、部署脚本写歪能让人在凌晨三点对着日志抓头发。SpringBoot 的打包部署算是老生常谈的话题了可我发现身边不少朋友包括一些写了几年业务代码的同行对 jar 包和 war 包这两种方式的取舍、内部机制、部署细节还是一知半解。有人习惯性打个 jar 扔服务器上 java -jar 就跑有人接手老项目发现必须丢进外部容器里才能活还有人打完包发现配置文件没生效、静态资源 404、端口被占用排查半天找不到北。这篇东西就是想把 SpringBoot 两种打包方式——可执行 jar 包和war 包——从原理到实操、从部署到发布、从踩坑到避坑一次性讲透。我会说清楚它们各自解决什么问题、内部是怎么运作的、什么场景该选哪个、打包配置怎么写、部署命令怎么敲、发布流程怎么设计以及那些文档里不会写但实际会咬你一口的细节。不管你是刚接触 SpringBoot 的新手还是想把手头部署流程理顺的老手应该都能从里面捞到点能直接抄作业的东西。1. 两种打包方式的本质差异与选型逻辑1.1 可执行 jar 包到底“可执行”在哪很多人以为 SpringBoot 打出来的 jar 就是普通 jar其实它跟传统 jar 包结构完全不是一回事。传统 jar 包你把 main 方法所在类丢进去得靠java -cp xxx.jar com.xxx.Main这样指定类路径和主类才能跑而 SpringBoot 的 jar 包直接java -jar就能启动秘密全在打包插件对 jar 结构的改造上。SpringBoot 的 Maven 插件会把项目类、依赖 jar 包、启动器类重新组织成一个特殊布局根目录下是BOOT-INF/classes放你自己的编译产物BOOT-INF/lib放所有第三方依赖org/springframework/boot/loader下放的是 SpringBoot 自己的类加载器而META-INF/MANIFEST.MF里通过Main-Class指向JarLauncher再通过Start-Class指向你真正的启动类。启动时JarLauncher先跑起来它用自定义的类加载器LaunchedURLClassLoader去加载BOOT-INF/lib和BOOT-INF/classes里的内容最后反射调用你的Start-Class。这个设计带来的直接好处是依赖全部内嵌一个 jar 包就是完整运行单元扔到任何装了 JRE 的机器上都能跑不依赖外部容器的类加载器也不会跟服务器上其他应用的依赖打架。代价是包体积大一个中等项目动辄几十上百兆而且每次改一行代码重新打包整个 fat jar 都得重新生成传输和启动都相对慢一些。1.2 war 包为什么还得靠外部容器war 包是 Java Web 领域的传统交付格式本质是一个符合 Servlet 规范的 Web 应用归档。SpringBoot 打 war 包时插件会把依赖放到WEB-INF/lib你的类放到WEB-INF/classes同时生成WEB-INF/web.xml或者用 Servlet 3.0 的注解方式注册并且把内嵌的 Tomcat 相关依赖标记为provided意思是“编译时需要但打包时不带运行时由外部容器提供”。这里的关键在于类加载器的归属。war 包丢进外部 Tomcat 后应用的类由 Tomcat 的WebappClassLoader加载Servlet 容器负责创建ServletContext、管理生命周期、处理请求分发。SpringBoot 应用要能在这种环境下启动需要一个引导类继承SpringBootServletInitializer并重写configure方法把主配置类注册进去这样容器启动时才能触发 Spring 的初始化流程。war 方式的好处是能复用外部容器已有的连接池、线程池、JNDI 资源、监控管理界面多个应用共享一个容器实例运维上跟传统 Java Web 项目保持一致。坏处是部署依赖容器版本容器配置不当会牵连应用而且 SpringBoot 的内嵌容器特性在 war 模式下基本用不上版本升级也受容器制约。1.3 选型不是拍脑袋看这几个维度到底选哪种别凭习惯按实际约束来。我整理了一个对照表把关键维度摆出来维度可执行 jarwar 包运行依赖只需 JRE需要外部 Servlet 容器部署单元单文件独立进程放入容器 webapps 目录类加载隔离应用自带类加载器隔离好受容器类加载策略影响多应用共享资源不共享各自独立可共享容器资源与 JNDI启动速度相对快无容器初始化开销容器启动加应用初始化运维习惯偏云原生、容器化偏传统企业运维体系版本耦合容器版本由应用自带受外部容器版本约束我的经验判断是新项目、微服务、要上容器编排平台的一律走 jar接手存量系统、公司运维体系强制要求统一丢 Tomcat、需要跟一堆老 war 应用共用容器资源的走 war。还有一种中间情况就是同一套代码既要能本地java -jar快速调试又要能打成 war 交付给运维那就得在pom.xml里做条件化配置这个后面细说。2. 打包配置的核心细节与实操要点2.1 jar 打包的 pom 配置与插件行为用 Maven 构建 SpringBoot 项目spring-boot-maven-plugin是核心。最简配置长这样build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build版本一般由父 pom 的spring-boot-starter-parent统一管理不用自己写。执行mvn clean package后target目录下会出现两个 jar一个是xxx.jar.original这是插件在改造前备份的原始 jar另一个是xxx.jar这才是可执行的 fat jar。很多人第一次看到.original文件会懵其实它就是普通 Maven 打出来的那个包插件把它重命名保留下来方便你做对比或者特殊场景使用。插件默认的repackagegoal 会绑定到package生命周期阶段自动完成 fat jar 的重新打包。如果你有特殊需求比如只想打普通 jar 不想 fat 化可以配置skiptrue/skip如果想指定启动类多模块项目里常见用mainClass指定如果项目里有多个 main 方法导致插件识别混乱也必须显式指定mainClass。有个细节值得说spring-boot-maven-plugin在 repackage 时会排除spring-boot-starter-tomcat吗不会jar 模式下内嵌 Tomcat 是正常打进去的这正是它能独立运行的原因。所以别看到BOOT-INF/lib里有 tomcat 的 jar 就以为配错了。2.2 war 打包必须改的三处地方打 war 包不是改个packaging就完事有三处必须动少一处都启动不了。第一处pom.xml的 packaging 改成 warpackagingwar/packaging第二处把内嵌 Tomcat 的作用域设为 provided让它在打包时被排除dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency第三处启动类要继承SpringBootServletInitializer并重写configureSpringBootApplication public class Application extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(Application.class); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这个configure方法是外部容器启动应用的入口容器通过 SPI 机制找到SpringServletContainerInitializer再由它回调你重写的configure把主配置类交给 Spring 去初始化。少了这一步war 丢进 Tomcat 后容器能起来但你的应用根本不会被加载访问就是 404。注意spring-boot-starter-web里已经传递依赖了 tomcat如果你单独再声明一个 provided 的 tomcat 依赖要确认没有版本冲突。稳妥做法是直接在spring-boot-starter-web上排除内嵌容器再单独引入 provided 版本或者干脆只保留一个显式的 provided 声明。2.3 一套代码兼容两种打包的条件化配置实际项目里经常遇到“开发用 jar 快速跑交付用 war”的需求。硬编码切换太蠢用 Maven profile 做条件化最干净。思路是定义两个 profile一个激活时打 jar 并保留内嵌容器另一个激活时打 war 并把容器设为 provided。profiles profile idjar/id activation activeByDefaulttrue/activeByDefault /activation properties packaging.typejar/packaging.type /properties /profile profile idwar/id properties packaging.typewar/packaging.type /properties /profile /profiles然后在依赖和打包配置里引用${packaging.type}做判断。不过说实话Maven 的 packaging 标签本身不支持变量替换所以更实际的做法是主 pom 固定一种 packaging另一种通过插件配置和依赖 scope 的 profile 切换来模拟。我见过更省事的方案是维护两个 pom 文件用-f指定构建虽然土但直观团队小的时候反而好维护。选哪种看团队习惯没有绝对优劣关键是别让构建方式成为只有某一个人懂的“黑魔法”。2.4 打包时容易忽略的资源与配置问题打包不只是编译代码资源文件的处理经常出岔子。src/main/resources下的配置文件默认会被打进classes或WEB-INF/classes但如果你用了多环境配置application-dev.yml、application-prod.yml打包时激活哪个 profile 决定了最终生效的配置。命令行mvn package -P prod里的-P是 Maven profile跟 Spring 的spring.profiles.active是两码事别搞混。前者控制构建行为后者控制运行时配置加载。静态资源方面src/main/resources/static和public目录下的内容会被 SpringBoot 默认映射但 war 模式下如果外部容器有自己的默认 Servlet 处理静态资源可能出现优先级问题。我遇到过一次 war 部署后 CSS 加载 404排查发现是容器配置了全局的静态资源拦截跟应用内的映射冲突了最后通过调整WebMvcConfigurer里的资源处理器顺序解决。还有application.properties里的端口配置jar 模式下server.port生效war 模式下这个配置会被忽略端口由外部容器决定。这个差异如果不清楚部署后访问错端口能查半天。3. 部署实操从命令到进程管理3.1 jar 包部署的完整流程拿到 fat jar 后部署流程其实不复杂但每一步都有讲究。假设包名是app.jar放在/opt/app目录下。第一步确认目标机器 JRE 版本。SpringBoot 3.x 要求 Java 17 起步2.x 一般 Java 8 或 11。用java -version核对版本不匹配启动直接报UnsupportedClassVersionError。第二步前台试跑确认能起来java -jar app.jar这一步别急着后台化先看启动日志有没有报错端口有没有被占用数据库连接通不通。确认无误再 CtrlC 停掉。第三步后台启动并重定向日志nohup java -jar app.jar app.log 21 nohup让进程忽略挂断信号 app.log 21把标准输出和标准错误都写进日志文件末尾放后台。这里有个坑21必须写在重定向目标之后顺序写反了错误输出不会进日志排查问题时日志里只有正常输出错误信息全丢了。第四步记录进程号方便管理echo $! app.pid$!是上一个后台进程的 PID。后续停止用kill $(cat app.pid)比ps -ef | grep java再手动找 PID 靠谱得多尤其服务器上跑着多个 Java 进程时。3.2 JVM 参数怎么配才不踩坑直接java -jar用的是 JVM 默认参数生产环境肯定不够。常见配置java -Xms512m -Xmx1024m -XX:UseG1GC -XX:MaxGCPauseMillis200 -jar app.jar-Xms和-Xmx设堆初始值和最大值建议设成一样避免运行时扩缩容带来的抖动。-XX:UseG1GC在大多数场景下是稳妥选择MaxGCPauseMillis给个目标值让 GC 自己调节。但别盲目抄网上的参数堆大小要结合机器内存和应用实际占用来定。我的习惯是先按物理内存的 50% 到 70% 给堆跑一段时间看 GC 日志和监控再调。还有个容易忽略的点-Dfile.encodingUTF-8。有些服务器默认编码不是 UTF-8日志里中文乱码排查半天以为是代码问题其实是 JVM 编码没指定。加上这个参数一劳永逸。3.3 war 包部署到外部容器的步骤war 部署的核心是把包放进容器的部署目录。以常见的 Tomcat 为例把app.war复制到$CATALINA_HOME/webapps/下启动容器即可。容器会自动解压 war 并以目录名作为 context path比如app.war对应访问路径/app。如果想部署为根路径直接/访问把 war 改名为ROOT.war放进去。这个操作要小心如果容器里已经有 ROOT 应用会被覆盖先备份。部署前要确认几件事容器的 Java 版本跟应用编译版本匹配容器的server.xml里 Connector 端口没被占用如果应用用了 WebSocket 或异步 Servlet容器要开启对应支持war 里的WEB-INF/lib依赖跟容器自带的库没有版本冲突尤其 Servlet API、JSP API 这类容器提供的包。部署后看容器日志logs/catalina.out应用启动信息会混在容器日志里。如果应用起不来先看有没有ClassNotFoundException或NoSuchMethodError这类多半是依赖冲突再看SpringBootServletInitializer有没有正确配置没配的话应用根本不会初始化。3.4 用 systemd 托管 jar 进程nohup方式简单但机器重启后进程不会自动拉起进程挂了也没人管。生产环境更稳的做法是用 systemd 托管。写一个 service 文件/etc/systemd/system/myapp.service[Unit] DescriptionMy SpringBoot App Afternetwork.target [Service] Typesimple Userappuser WorkingDirectory/opt/app ExecStart/usr/bin/java -Xms512m -Xmx1024m -jar /opt/app/app.jar SuccessExitStatus143 Restarton-failure RestartSec10 StandardOutputappend:/opt/app/app.log StandardErrorappend:/opt/app/app.log [Install] WantedBymulti-user.target几个关键点SuccessExitStatus143是因为 JVM 收到 SIGTERM 正常退出时返回码是 143不声明的话 systemd 会认为进程异常退出Restarton-failure让进程崩溃后自动重启RestartSec控制重启间隔避免疯狂重启刷日志。配好后systemctl daemon-reload再systemctl enable --now myapp就托管上了开机自启、崩溃自愈都有了。4. 发布流程设计与常见问题排查4.1 一次靠谱的发布该有哪些环节发布不是把包传上去重启就完事中间少了环节迟早出事。我习惯把发布拆成几个固定动作构建产物校验、传输、备份旧版本、停旧进程、启新进程、健康检查、失败回滚。构建产物校验是第一步确认打出来的包是这次要发的版本别把上次的旧包传上去。可以在构建时把 git commit 号或时间戳写进MANIFEST.MF部署前核对。传输用scp或内网制品库拉取传完比对文件大小和校验和。备份旧版本很简单把当前运行的 jar 复制一份带时间戳的副本出问题能秒回滚。停旧进程用前面记录的 PID 或 systemd 的 stop等进程真正退出再启新的别 stop 完立刻 start端口可能还没释放。健康检查是很多人漏掉的环节。应用进程起来了不代表服务可用SpringBoot Actuator 的/actuator/health端点能反映应用真实状态。启动后轮询这个端点返回UP才算发布成功超时或返回异常就触发回滚。这个检查能拦住大量“进程活着但服务挂了”的情况比如数据库连不上、依赖服务不可达。4.2 端口占用与启动失败的排查路径端口被占用是最常见的启动失败原因报错信息一般是Port 8080 was already in use。排查用lsof -i:8080或netstat -tlnp | grep 8080找到占用进程确认是不是上次没停干净的旧实例是就杀掉不是就换端口或协调。如果端口没问题但启动卡住看日志停在哪一步。卡在数据库连接池初始化多半是数据库地址、账号密码或网络不通卡在某个PostConstruct方法是初始化逻辑里有阻塞操作卡在 Tomcat 启动可能是 SSL 证书配置或连接器参数有问题。日志是排查的第一手材料所以前面强调日志重定向别写错。还有一种隐蔽情况应用启动成功但访问 404。jar 模式下检查server.servlet.context-path配置war 模式下检查访问路径是否带了 context pathwar 包名。很多人 war 部署后直接访问/接口路径忘了前面要加应用名白白排查半天。4.3 依赖冲突在两种模式下的不同表现依赖冲突在 jar 和 war 模式下表现不一样这点值得单独说。jar 模式下应用自带类加载器依赖全在BOOT-INF/lib里冲突主要发生在应用内部不同依赖引用了同一个库的不同版本Maven 的依赖调解就近原则会选一个选错了就NoSuchMethodError。用mvn dependency:tree看依赖树排除掉多余版本即可。war 模式下多了一层容器类加载器。容器的lib目录下有一批公共库Servlet API、JSP API、部分日志库应用的WEB-INF/lib是另一批。容器类加载策略默认是“父优先”还是“子优先”会影响加载结果Tomcat 默认对部分包是父优先如果应用里带了跟容器版本不同的同名库可能加载到容器的旧版本报错信息往往很迷惑。解决办法是在WEB-INF下放context.xml配置类加载策略或者干脆把冲突的库排除掉用容器提供的版本。4.4 常见问题速查表把实际踩过的坑整理成表遇到问题先对号入座现象可能原因排查与解决java -jar报无主清单属性打的是普通 jar 不是 fat jar确认spring-boot-maven-plugin生效用 repackage 后的包启动报UnsupportedClassVersionErrorJRE 版本低于编译版本升级目标机 JRE 或降低编译 targetwar 部署后访问 404未继承SpringBootServletInitializer补上继承和configure重写war 部署后端口配置不生效war 模式端口由容器决定改容器 Connector 端口忽略应用内server.port日志中文乱码JVM 编码未指定启动加-Dfile.encodingUTF-8后台启动后错误信息丢失重定向顺序写反用 app.log 21正确顺序进程崩溃不自动拉起未做进程托管用 systemd 配置Restarton-failure依赖冲突NoSuchMethodError多版本库共存mvn dependency:tree定位并排除静态资源 404容器与应用资源映射冲突调整资源处理器或容器配置发布后服务不可用但进程在初始化失败或依赖不可达接入 Actuator 健康检查并轮询4.5 几个只有踩过才知道的实操心得说几个文档里不会写、但实际很省事的经验。第一打包时在MANIFEST.MF里写入构建时间和版本号部署前unzip -p app.jar META-INF/MANIFEST.MF一眼就能确认包对不对比看文件名靠谱。第二nohup启动的进程如果日志文件不切割跑几个月能撑爆磁盘要么配 logback 的滚动策略要么用 logrotate 管理别等磁盘满了才发现。第三war 部署前先在本地用相同版本的容器跑一遍容器版本差异导致的问题在本地暴露比在生产暴露成本低得多。第四回滚脚本要提前写好并测试真出事的时候没时间现写备份文件命名带时间戳回滚就是复制加重启两步。发布这件事工具和命令都是次要的关键是流程里每个环节都有确认和兜底。打包方式选对、配置写全、部署有托管、发布有检查和回滚这套下来基本能睡个安稳觉。至于 jar 还是 war回到最初那句话新体系走 jar存量体系走 war一套代码要两头兼顾就用 profile 或双 pom 把差异隔离干净别让构建方式变成团队里只有一个人能碰的禁区。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询