
刚接手一个旧项目维护发现整个团队还在用war包部署SpringBoot我没忍住提了一句“这年头不都打jar包直接run吗”结果被维护这套系统的老同事拉着讲了半小时为什么它们必须留在Tomcat里。等我把这套流程完整走了一遍才发现war包部署这事儿在真实生产环境里压根没过时——有存量Tomcat规范、有统一运维入口、有安全审计要求的场景下war包反而是最务实的方案。这篇就把整个流程从项目改造、打包、上传到远端Linux、启动验证再到排坑完整捋一遍。纯实战总结适合正在接触公司旧系统、或者第一次要把SpringBoot塞进Tomcat的人参考能帮你少踩一半坑。1. 为什么在SpringBoot时代还用war包先聊聊决策问题。SpringBoot默认打jar包内嵌Tomcatjava -jar一下就能跑这在本地开发和微服务部署里确实干净利落。但war包部署的核心价值不在“能跑”而在于“符合谁的规矩”。遇到下面这几种情况war包反而是唯一合理的选择公司运维体系已经固定所有Java应用统一部署到一台Tomcat由专门的运维账号管理启动脚本、日志切割和监控告警。你丢一个jar包过去运维反而不知道该怎么接。这台服务器上可能存在多个应用共享一个Tomcat实例。虽然官方不推荐这种用法但老企业环境里这就是事实标准一个新系统想单独占用端口需要走一堆工单审批。安全基线要求所有应用的启动方式、日志目录、临时目录均由统一组件管理不允许开发人员随意用java命令启动。历史代码里有些模块还是Servlet、Filter那一套和SpringBoot混编直接打jar包在某些场景下会出现类冲突而交给Tomcat的类加载机制反而是最稳的。当然war包也有明显的短板最明显的就是启动慢、资源占用高、部署时需要整个Tomcat配合你重启不像jar包那样进程级可随意启停。但你要是反问一句“这些短板在我这个场景下算不算问题”答案往往是不算。搞清楚这一点后面的技术操作才有意义。2. 改造SpringBoot项目从jar思维切到war思维很多人第一次改war包以为把pom.xml里packaging换成war就完事了。等打包完成丢进Tomcat的webapps目录启动一看要么404要么提示找不到主类要么本地一把梭跑得飞起、一到Tomcat里就各种幺蛾子。问题基本都是下面这三个点没处理好。2.1 pom.xml调整packaging、依赖排除、Servlet依赖首先把默认的打包方式改成warpackagingwar/packaging然后处理SpringBoot内嵌Tomcat的依赖冲突。因为war包最终由外部Tomcat来跑内嵌Tomcat的依赖必须排除掉否则容易出现jar包冲突或者启动时加载了重复的Servlet容器。注意不要排除spring-boot-starter-web里的web功能本身只排除其中内嵌的Tomcatdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency同时需要引入Servlet API的依赖因为最终跑在外部Tomcat里编译阶段需要Servlet相关接口dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId scopeprovided/scope /dependency注意版本。如果是Spring Boot 2.x对应的是javax.servlet坐标Tomcat 9用这个没问题。但Spring Boot 3.x已经全面切换到jakarta.servlet且外置Tomcat必须是Tomcat 10及以上。这一条踩坑率极高后面专门展开说。排除内嵌Tomcat之后本地还能不能直接跑当然能IDE里跑SpringBoot时classpath上压根没有内嵌Tomcat但SpringBoot自己已经实现了Servlet容器启动逻辑IDE识别到SpringApplication.run照常引导——只有当你把war包部署到外部Tomcat时外部容器才接管启动流程。这也就是为什么很多人改完pom本地还能跑完全没有异常感知。2.2 改造启动类继承SpringBootServletInitializerwar包模式的核心就是让外部Tomcat能找到你的SpringBoot入口。需要把启动类改为继承SpringBootServletInitializer并重写configure方法SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(DemoApplication.class); } }为什么必须这么做外部Tomcat启动时通过Servlet 3.0的ServletContainerInitializer机制去寻找SpringBootServletInitializer子类然后由它加载Spring上下文。如果不继承这个类Tomcat只把你这个项目当普通Servlet应用启动Spring的所有配置都不会被加载结果就是访问接口全部404。有的老项目里启动类如果被别的类继承了或者类的结构比较特殊也可以采用组合方式单独写一个初始化类来注册ApplicationContext但大多数情况下直接继承就完事不要搞花活。2.3 application.properties与内置Tomcat端口的关系war包部署后一个最容易让人懵的配置就是server.port。很多人习惯在配置文件里写server.port8080jar包模式下这个端口直接生效到了war包模式下这个端口会被Tomcat忽略——因为此时监听端口的角色变成了外部Tomcat。也就是说你在war包里写的server.port只在本地IDE里用内置方式跑的时候有效。一旦部署到Tomcat中实际访问端口由Tomcat的server.xml决定。这一点不提前讲清楚部署完按自己项目里配置的端口去访问基本都会吃一记404或连接拒绝。如果你的项目里存在多个环境比如本地、测试、生产建议把server.port相关配置注释掉或者只在application-local.properties里启用避免后续有人误以为改这个就能调整线上端口。2.4 静态资源和上传目录路径的差异jar包模式下ClassPathResource指向的是jar包内部的路径。war包部署后classes目录直接落在Tomcat的WEB-INF/classes下路径结构和之前完全不一样。如果你在代码里用了new File(src/main/resources/...)或者把文件写到资源目录线上必然出问题。正确的做法是把所有需要写入的路径改成绝对路径或者通过配置项注入目录路径再或者用ServletContext.getRealPath来获取真实路径。3. 远端Linux服务器环境准备与检查war包部署对服务器侧的依赖其实很简单一个能跑的JDK一个对应版本的Tomcat以及确认端口、防火墙、目录权限。但就是这几样现场环境五花八门准备流程必须有一份checklist。3.1 JDK版本检查别用错了运行时SpringBoot 2.x要求JDK 8及以上SpringBoot 3.x要求JDK 17及以上。上服务器第一步先确认java -version如果你发现服务器上存在多个JDK比如系统自身带了一个OpenJDK 1.7而你又装了JDK 8或17那么必须先确认Tomcat用的是哪个Java环境。Tomcat的bin/setenv.sh里可以显式指定JAVA_HOME如果没有这个文件默认取系统的JAVA_HOME。强烈建议在Tomcat的bin目录下创建setenv.sh把JAVA_HOME写死export JAVA_HOME/usr/local/jdk1.8.0_202 export PATH$JAVA_HOME/bin:$PATH不然某次服务器重启或环境变量变动Tomcat可能突然用错JDK版本启动报Unsupported major.minor version排查起来够喝一壶。3.2 Tomcat版本选择SpringBoot 2.x与3.x的分水岭这一步非常关键说直白点就是Tomcat的大版本必须和SpringBoot的Servlet API版本对得上。项目Servlet API坐标适配Tomcat版本SpringBoot 2.xjavax.servletTomcat 8.5 / 9.xSpringBoot 3.xjakarta.servletTomcat 10.0如果SpringBoot 3.x工程打出来的war包放进Tomcat 9里启动不会报错但所有接口都404且日志里能看到org.apache.catalina.core.StandardContext相关的类加载异常提示。Tomcat 9用的是javax.servlet命名空间而SpringBoot 3.x的类全是jakarta.servlet命名空间压根对不上。这一条一定要在打包之前确认好而不是在部署之后才排查。3.3 目录与端口规划Tomcat一旦承载war包webapps目录就是你整个发布流程的主战场。建议整个部署目录和日志目录规范化/usr/local/tomcat/webapps/war包存放处/usr/local/tomcat/logs/Tomcat运行日志/opt/applogs/业务日志的统一目录/opt/appbackup/历史版本备份端口方面默认8080。如果你要换端口直接改server.xml里的Connector port。但注意云服务器上的安全组规则、服务器防火墙iptables/firewalld需要同步放行端口不然本地8080没问题远端访问就超时。这种低级问题真的非常常见我见过有人排查了半天代码最后发现是防火墙没开。4. 打包、上传、部署完整实操记录这章节是整个流程的主干我按实际执行顺序一步步写包括每步的核心命令和为什么这么做。4.1 本地打包war包先到项目根目录执行构建命令mvn clean package -DskipTests这里特别说下-DskipTests和-Dmaven.test.skiptrue的区别。前者会编译测试代码但不运行后者连测试代码都不编译。如果只想快速打包建议直接-Dmaven.test.skiptrue省掉编译测试的时间尤其是老项目里测试代码有一堆依赖的时候速度差距很明显。构建完成后在target目录下会生成一个xxx.war。先去确认一下war包内容jar tf target/xxx.war | head -20你会看到标准的war包结构重点确认WEB-INF/classes/下有没有application.propertiesWEB-INF/lib下有没有你的业务依赖包。如果发现配置文件没进去多半是maven-resources-plugin的过滤配置有问题。4.2 上传war包到Linux服务器的三种方式如果服务器开放了SSH最直接的方式是scpscp target/xxx.war root192.168.x.x:/usr/local/tomcat/webapps/如果是跳板机环境不能直接scp到目标机器可以先传到跳板机再从跳板机内网传一次。如果公司有发布平台运维统一发布那就按发布平台的上传流程走。但不管哪种方式上传前后都建议对一下文件大小用md5sum校验一下避免传了个残缺war包启动半天报一个很奇怪的错。md5sum xxx.war4.3 解压与启动Tomcat到底是怎么跑war包的Tomcat有一个特性当webapps目录下出现同名war包时Tomcat启动时或者运行时检测到war包变化会自动将它解压成同名的目录。也就是说你把xxx.war放进webappsTomcat会自动解压成webapps/xxx/然后从目录启动应用。所以有个很常见的做法更新版本时不用手动删除解压目录直接把新war包覆盖上传然后重启Tomcat。但这样做的隐患在于旧的解压目录里可能残留一些旧文件干扰新版本运行。稳妥的做法是先停Tomcat删掉旧war包和旧解压目录上传新war包再启动。# 停止Tomcat /usr/local/tomcat/bin/shutdown.sh # 确认进程退出后清理 ps -ef | grep tomcat rm -rf /usr/local/tomcat/webapps/xxx.war /usr/local/tomcat/webapps/xxx # 上传新war包后启动 /usr/local/tomcat/bin/startup.shshutdown.sh执行完并不代表进程一定退出了。如果服务器上有其他Tomcat管理脚本务必确认进程真的kill掉再执行后续操作否则会出现新war包还没解压完旧进程已经开始写日志两边文件互相踩踏的情况。4.4 启动验证别只看Tomcat活没活第一步确认Tomcat进程存在ps -ef | grep tomcat第二步看Tomcat启动日志tail -f /usr/local/tomcat/logs/catalina.out正常情况会看到类似信息INFO: Deployment of web application archive [xxx.war] has finished同时会打印出SpringBoot的启动日志包括Tomcat started on port(s)等。但要注意外置Tomcat环境下SpringBoot日志里不会出现正常启动时的Tomcat started on port 8080这类内置容器专属信息。所以很多人看到启动日志没有这句就开始慌其实无所谓只要看到Spring上下文加载完成、Started关键字出现就说明应用已经挂到外部Tomcat上了。第三步验证接口是否真的通了curl http://127.0.0.1:8080/xxx/health务必先在本机回环地址验证一次排除掉远端网络因素干扰。如果curl返回200或统一的JSON结构部署这一步才算真正完成。4.5 context path的坑访问路径为什么会多一层这是war包部署最容易让人困惑的地方。JavaWeb应用的访问路径默认是http://ip:端口/war包名/业务路径比如war包名是xxx那应用根路径就是/xxx。如果你在SpringBoot项目里配置了server.servlet.context-path/demo那么war包部署时访问路径会变成http://ip:端口/xxx/demo/...两层前缀叠在一起。处理方案有两个方向让war包名和context path保持一致最简单的是直接改war包名比如改成demo.war然后配置文件里server.servlet.context-path/demo保持不变。如果你想去掉war包名只保留业务路径可以把war包改名为ROOT.war这样Tomcat会把应用挂在根路径下访问地址就是干净的http://ip:端口/业务路径。实际项目中ROOT.war方案也能用但有个副作用一台Tomcat只能有一个ROOT.war多应用共用Tomcat时就只能靠不同war包名区分路径。生产环境里建议保留war包名因为运维和监控会依据这个路径来做健康检查私自改成ROOT有时候会和其他系统的路径约定冲突。5. 实战中一定会遇到的问题排查思路与心得把几个我踩过的坑、见过同事踩过的坑集中列一下。每个都很典型照着排查能节省大量时间。5.1 启动时内存溢出PermGen和MetaspaceJava 8以后的JVM使用Metaspace但老代码迁移到新JDK时经常碰到元空间不够的情况。如果你在Tomcat里同时部署了几个大型war包默认的JVM参数大概率不够用。通过bin/setenv.sh里重新定义export CATALINA_OPTS-Xms1024m -Xmx2048m -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m有个细节CATALINA_OPTS和JAVA_OPTS的作用不同。Tomcat启动时JAVA_OPTS作用于Tomcat本身进程会传给所有由Tomcat启动的JVM而CATALINA_OPTS只作用于Tomcat主进程。部署war包时建议用CATALINA_OPTS这样配置的JVM参数不会被某些特殊场景比如remote debug干扰到。5.2 接口404但服务启动成功查这四件事war包部署后接口404优先按顺序查访问路径是否正确是不是缺少war包名前缀。shutdown.sh后旧进程是否真的退出如果之前的进程还在可能你访问的是旧应用。是否存在重复的SpringBootServletInitializer类如果项目里有多个类都继承了它Tomcat可能加载了错误的一个。确认SpringBoot版本和Tomcat版本匹配尤其SpringBoot 3.x Tomcat 9必挂。5.3 定时任务重复执行上线war包后发现原本一天执行一次的定时任务突然执行了两次。原因基本是服务器上旧进程没完全停掉新旧两个Spring容器同时活着两个实例各跑了一套定时任务。解决方法是重启前稳定确认进程死透可以主动用jps查进程。5.4 application.properties外置改配置不用重新打包war包部署最麻烦的体验之一就是改一个数据库地址就要重新打包上传。很多团队因此把配置文件外置在setenv.sh里添加export CATALINA_OPTS$CATALINA_OPTS -Dspring.config.location/opt/config/application.propertiesSpringBoot在启动时会优先加载这个外部配置war包内的配置文件就会被覆盖。不过要注意如果配置项较多外置文件需要维护版本和权限建议配合运维的配置中心一起管理避免服务器上散落一堆没有版本记录的配置文件。5.5 日志输出路径的问题jar包模式下日志一般和jar包同目录war包部署后在Tomcat目录下日志路径会变得比较乱。建议在logback或log4j2配置里用绝对路径而不是相对路径。比如property nameLOG_HOME value/opt/applogs/xxx /这样部署位置怎么变日志总能留在预期的目录也方便日志收集系统去拉取。6. 从war包出发聊聊部署方式的选择走到这一步war包部署的基本流程已经完整跑通了。个人在实践中的体会是部署方式从来不是越新越好而是越匹配团队运维模式越好。war包部署省掉了进程守护、启动脚本编写、日志目录约定这些环节直接靠在Tomcat这套成熟体系上对于维护力量薄弱的团队反而是省心方案。最后分享一个延展技巧war包部署的应用其实也能做灰度发布。利用Tomcat的热部署特性把新版本war包命名为xxx##new.war上传Tomcat会在不重启的情况下启动一个新上下文验证OK后再切换流量。当然这要求你的代码没有兼容性破坏数据库迁移也要做到向后兼容否则换上去直接白屏。这个操作不太适合零基础的用户但值得作为后续优化方向等基础流程完全跑顺之后再去尝试。还是那句话方案是死的场景是活的。war包有它的视觉疲劳感但能在存量环境里稳定跑上几年不出幺蛾子的方案就是好方案。