资讯动态

Spring Cloud多模块微服务项目war包部署Tomcat完整指南

发布时间:2026/10/14 1:37:12 来源:尧图企业网站定制
我之前接手过一个Spring Cloud多模块微服务项目平时在开发环境用jar包启动IDEA里点一下就跑起来了自测联调都很顺利。结果到了预上线阶段运维扔给我一句话所有服务必须打成war包部署到统一Tomcat里。我当时是真的愁——Spring Boot默认不就是jar包吗微服务不就应该独立起进程吗但客户现场就是这么要求的再不合理也得想办法落地。我硬着头皮研究了两天踩了无数坑终于把SpringCloud微服务多模块打war包部署到tomcat这件事完整跑通了。这篇就把整个过程写下来包括pom怎么改、启动类怎么处理、多模块依赖怎么打包以及部署后容易踩的端口、注册中心、日志这些坑给正在为同样问题头疼的同学一份可以直接抄的作业。1. 项目背景为什么好好的jar不用非要打成war1.1 这种需求在真实项目中很常见很多开发者觉得Spring Boot天生就是打jar包环境里装个JDK直接java -jar就行为什么还要退回war包放到Tomcat我在项目里遇到的真实原因其实很简单公司现有的运维体系包括监控、日志采集、发布工具、端口管理全都是围绕Tomcat建的。某人说得很直白“你可以跑jar但出了问题自己解决监控和日志我们不看。”在这种情况下强上jar部署会有很大的落地阻力。还有一种情况是历史包袱。项目里有些老模块本来就是war包部署在同一个Tomcat上新拆出来的Spring Cloud服务如果跑在旁边运维同学需要维护两套方式一套是Tomcat一套是命令行进程。为了统一他们宁愿所有新服务也打成war。说白了这是一个“非技术正确但项目现实”的选择。所以如果你也碰到这种需求别觉得奇怪先把活儿干好更重要。1.2 多模块项目打war的难点到底在哪Spring Cloud微服务通常不是一个单独的Spring Boot应用而是一个Maven多模块工程。常见结构是一个父pom下面挂着若干公共模块和业务服务模块。比如common模块放通用类、工具类、DTOservice-a和service-b是真正的微服务它们都依赖common。这样多模块结构打war时问题就来了war包必须把common的代码和依赖一起打进去否则部署到Tomcat里就会出现NoClassDefFoundError。另外Spring Cloud服务还要注册到注册中心。如果你有好几个服务而且都要放在同一个Tomcat实例里服务之间的调用URL会涉及端口和contextPath的匹配。这些细节单测和本地jar包启动时完全不会被发现只有真正部署到Tomcat后才会炸出来。所以表面上是“把jar改成war”实际上是“改构建方式、改启动方式、改注册方式、改日志路径”一连串问题。这篇文章的核心就是把这些环节一一讲透。2. 打包前的工程梳理与依赖结构2.1 先看清你的Maven模块关系动手改pom之前我强烈建议你先在父工程目录下执行一遍mvn dependency:tree这个命令会把当前模块的依赖树完整列出来。多模块工程里最常见的依赖关系是这样的parent-project (pom) ├── common (jar) ├── service-a (spring boot, 最终war) └── service-b (spring boot, 最终war)父工程只是用来统一管理依赖版本打包类型是pom。common模块会被打成jar作为服务模块的依赖被引用。真正需要打成war的是service-a、service-b这些包含main方法、带SpringBootApplication的模块。搞清楚这个关系非常重要因为打包时要保证common模块先被安装到本地仓库或者用Maven参数同时构建它。否则你单独跑到service-a目录下执行mvn packageMaven去本地仓库找common的jar如果找不到直接报错Could not resolve dependencies for project com.example:service-a:war:1.0-SNAPSHOT这个坑在第一次尝试打war时特别容易出现。2.2 Spring Boot版本决定了能用的Tomcat版本Spring Boot 2.x和Spring Boot 3.x在Servlet这块有本质区别。Spring Boot 2.x使用的是javax.servlet包外部Tomcat用8.5或9.0都能跑Spring Boot 3.x换成了jakarta.servlet包必须用Tomcat 10及以上否则启动时直接报NoClassDefFoundError: jakarta/servlet/Servlet。我就是因为这个原因被卡了一晚上最后排查到现场Tomcat版本还是8.5但项目是Spring Boot 3.2两个根本不兼容。所以动手之前先确认两件事项目里的Spring Boot版本是2.x还是3.x目标环境里的Tomcat版本是多少如果是Spring Boot 2.x Tomcat 8.5/9.0问题不大如果是Spring Boot 3.x必须确保Tomcat版本是10以上。另外如果项目里用了javax的第三方依赖即使Spring Boot 3.x也会面临迁移问题。这个在打war之前就要先过一遍否则改完pom才发现底层版本不对等于白改。2.3 打包插件的本质不是所有war都能直接部署这里要先搞清楚一个概念Spring Boot项目打的war和传统Web项目的war并不是一回事。传统war结构很简单就是WEB-INF/classes、WEB-INF/lib、META-INF等目录Spring Boot在war基础上又增加了一层“可执行war”的结构。它靠Spring Boot Maven插件的repackage功能把应用类和所有依赖重新整理生成一个既能java -jar直接运行也能丢到外部Tomcat里启动的war包。如果项目里没有配置spring-boot-maven-plugin那你打出来的war就是普通war里面可能只有自己的业务代码依赖jar一个都没进去。部署到Tomcat后能解压但一启动就报ClassNotFoundException因为Tomcat在WEB-INF/lib里找不到Spring相关jar。判断war是否打包成功的方法很简单打完包后执行unzip -l service-a.war | grep WEB-INF/lib | head如果WEB-INF/lib下面有几十个jar文件说明依赖已经被打进去了。如果只有一两个文件甚至为空那基本就是没做repackage。3. 一步步改造从jar到war的完整实操3.1 修改服务模块的packaging并排除内嵌Tomcat改造的第一步是把服务模块的packaging从默认的jar改成war。在service-a模块的pom.xml里加上packagingwar/packaging然后处理内嵌Tomcat。Spring Boot的spring-boot-starter-web默认带了一个内嵌Tomcat这个Tomcat在jar方式启动时很舒服但放到外部Tomcat里就成了麻烦。两个Tomcat同时存在会抢端口、加载重复类启动时各种报错。所以需要在spring-boot-starter-web里把内嵌Tomcat排除掉再单独引入一份设置为provided作用域的Tomcat依赖让它在打包时进到一个特殊目录不参与外部Tomcat的运行。配置如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependency为什么不能只加provided而不做排除因为Maven依赖的传递性规则里spring-boot-starter-web原本传递过来的spring-boot-starter-tomcat是compile作用域。就算你另外声明了provided作用域的同名依赖Maven在默认情况下还是会保留compile的那份两份都进war包。这会导致repackage后内嵌Tomcat依然落在WEB-INF/lib外部Tomcat启动时就会加载到内嵌容器最后报端口冲突或者LifecycleException。再配置Spring Boot插件build finalNameservice-a/finalName plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /buildfinalName我故意去掉了版本号目的是让war包名干净。因为Tomcat默认会用war包名作为访问路径的contextPath比如service-a.war对应http://ip:8080/service-a。如果war包叫service-a-1.0-SNAPSHOT.war访问路径也要跟着带上版本号配置Nginx和网关时特别难受。统一成不带版本号的名字后面会省很多事。3.2 继承SpringBootServletInitializer改启动类这是很多人最容易漏掉的一步。Spring Boot应用打包成jar时通过main方法跑起来就行但部署到外部Tomcat后Tomcat不会去执行你的main方法它要通过Servlet容器初始化机制来启动Spring上下文。想要让Tomcat认识你的Spring Boot应用启动类必须继承SpringBootServletInitializer并重写configure方法。改造后的启动类长这样SpringBootApplication public class ServiceAApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(ServiceAApplication.class); } public static void main(String[] args) { SpringApplication.run(ServiceAApplication.class, args); } }这里要注意包的路径。Spring Boot 2.x用的是import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;Spring Boot 3.x也一样这个类从2.x开始就在这个包下所以版本兼容性不用太担心。但如果你是从老项目迁移网上有些文章会写org.springframework.boot.web.support.SpringBootServletInitializer那个是Spring Boot 1.x的老路径用了必报ClassNotFoundException。还有一个经验一个war包里只需要有一个继承SpringBootServletInitializer的启动类就够了。如果多个类都继承了它Tomcat启动时会初始化多个Spring上下文很可能互相干扰。尤其是多模块项目里如果common模块里也放了一个带SpringBootApplication的类赶紧改掉不要让公共模块变成可启动模块。3.3 多模块Maven打包的正确姿势-pl -am 的妙用打包命令是这次操作里最大的坑之一。第一次我直接进到service-a目录执行mvn clean package -DskipTests结果报依赖找不到。原因就是common模块还没有install到本地仓库Maven在service-a目录下构建时不会自动去父工程其他模块里找依赖。正确做法是在父工程根目录执行mvn clean package -pl service-a -am -DskipTests参数拆解一下-pl表示只构建指定模块-am表示同时构建指定模块依赖的其他模块-DskipTests跳过测试节省时间这样Maven会先构建common再构建service-a并且service-a打war时能从本地仓库找到common的jar。如果项目里还有多个服务模块要一起打包比如service-a和service-b可以这样写mvn clean package -pl service-a,service-b -am -DskipTests打包完成后war包生成在对应模块的target目录下比如service-a/target/service-a.war。这个时候建议立刻验证一下war包体积如果明显偏小比如只有几十KB那大概率是漏了spring-boot-maven-plugin。正常的Spring Boot war包因为有依赖怎么看也有好几十MB。多模块工程还有一种情况如果公共模块改了代码你不想每次都用-am重新构建那就先执行一次mvn clean install -DskipTests把common安装到本地仓库之后再单独打包服务模块。3.4 部署到Tomcatwar包名、contextPath和注册中心地址部署方式比较老派把war包复制到Tomcat的webapps目录下启动Tomcat后会自动解压部署。如果你用的是Tomcat Manager也可以直接在管理界面里上传但生产环境一般不给你开Manager权限所以最常用的还是复制war包。war包名决定contextPath这一点我在前面提过。如果你把war命名为service-a.war部署后应用访问路径就是http://ip:8080/service-a如果命名成ROOT.war那访问路径就是http://ip:8080/这两个方案在Spring Cloud场景下各有坑。先说ROOT.war。好处是contextPath干净注册中心上报的地址很简单其他服务调用时不用处理路径前缀。坏处很明显同一个Tomcat只能有一个ROOT。如果你打算把多个微服务全塞进一个Tomcat让service-a做ROOTservice-b就只能带前缀了。再说不带版本号的service-a.war。多个服务可以共用一个Tomcat每个war一个名字各自有独立的contextPath。这时候要注意注册中心上报的服务地址默认是不带contextPath的。比如service-b通过Feign调用service-a它从注册中心拿到的地址可能是http://ip:8080然后去请求http://ip:8080/xxx而实际接口在http://ip:8080/service-a/xxx结果就是404。这个问题的解决办法我推荐两种每个服务独享一个Tomcat实例war包统一命名为ROOT从哪里都能规避contextPath问题如果必须共享Tomcat在服务配置里显式设置server.servlet.context-path并确保网关和Feign调用时带上这个前缀我最终采取的是第一种每个服务一个Tomcatwar包都叫ROOT。代码不需要改配置不需要迁就部署后一切正常。如果你的环境不允许每服务一个Tomcat那就得花更多时间在路由和注册地址上调试这个心理准备要有。3.5 外部Tomcat的额外配置端口和依赖目录还有一个小细节是Tomcat的lib目录。传统项目有时候会把部分公共依赖放到$CATALINA_HOME/lib下让多个应用共享。但Spring Boot项目最好不要这么干因为不同服务可能依赖不同版本的Spring类库放在共享目录里会导致类加载冲突。Tomcat的lib目录里只需要保留Servlet API等基础的容器类库Spring相关依赖都别放进去。端口方面Spring Boot应用部署到外部Tomcat后实际监听端口就是Tomcat的连接器端口通常是8080。如果你的业务服务配置里server.port写的是8081那它并不会在8081上开启一个新端口这个配置只在jar包启动时生效。但注册中心在收集服务地址时可能会读到这个8081配置最后其他服务拿着8081去访问自然连接不上。最好的做法是统一Tomcat端口和server.port保持一致避免这种隐性坑。4. 部署后必查的4个坑配置、日志、依赖和版本4.1 启动直接报错的常见错误速查表我把自己和身边同事踩过的坑整理成了一张表遇到问题时可以按表排查错误现象可能原因解决方案Port 8080 was already in use内嵌Tomcat没有排除和外部Tomcat冲突排除starter-web中的tomcat并设置tomcat依赖为providedLifecycleException: Failed to initialize componentTomcat lib下存在Spring相关jar导致类重复清空Tomcat lib下非Servlet API的jarNo Spring WebApplicationInitializer detected启动类没有继承SpringBootServletInitializer修改启动类重写configure方法ClassNotFoundException: org.springframework.web...war包缺少依赖可能是没有repackage确认spring-boot-maven-plugin已配置重新打包Could not resolve dependencies... common:jar公共模块没有install或使用-am构建在父工程目录加-am参数打包jakarta.servlet相关类找不到Spring Boot 3.x用了Tomcat 8.5换成Tomcat 10及以上这里面最值得单独说的是第一行。很多人只改了packaging为war没排除内嵌Tomcat结果Tomcat启动时报端口占用。我当时第一反应是查宿主机有没有别的进程占用了8080用netstat查了半天没查到最后发现是war包里的内嵌Tomcat也尝试启动两个Tomcat撞在一起。这个问题不看war包内部结构根本发现不了。4.2 服务注册中心端口对不上网关路由404Spring Cloud服务部署到Tomcat后最典型的故障是服务能启动、注册中心也能看到服务但服务间调用就是不通。我遇到过一次service-b调service-a时报Connection refused。去Nacos控制台一看service-a实例的端口是8081而Tomcat实际监听的是8080显然对不上。原因就是刚才说的server.port配置问题。Spring Boot在jar包启动时server.port直接决定监听端口但在外部Tomcat里这个端口配置失效。注册中心从Spring上下文里拿到端口可能是配置的8081也可能是默认的8080甚至在某些版本里会变成-1不同版本行为还不一样。我的处理思路很朴素让Tomcat端口、配置里的server.port、注册中心的端口三者完全一致。如果项目中所有服务都在同一个Tomcat里那就共用一个端口用contextPath区分如果每个服务独立Tomcat那就各自用各自的端口war包名统一为ROOT。这样注册中心拿到的地址就一定是实际可通的地址不会出现端口对不上的问题。除了端口还有一个常被忽略的是健康检查。service-awar包部署后建议先手动验证一下健康检查接口能不能通。比如curl http://localhost:8080/actuator/health如果这个接口返回UP再让其他服务去调基本上就能确定网络链路是通的。如果健康检查都404那问题大概率还是contextPath配置问题先把路径理顺。4.3 公共模块的类找不到war包体积异常多模块项目打war时“类找不到”和“war包体积异常”这两个问题往往是一起出现的。war包只有几十KB部署后启动报ClassNotFoundException十有八九是spring-boot-maven-plugin没有生效。出现这种问题先回看一下pom的build节点。如果服务模块里连这个插件都没有加只是加了packagingwar/packaging那Maven只会按普通war的方式来打包Spring Boot不会执行repackage依赖根本不会完整打入WEB-INF/lib。如果插件加了但war体积还是不对那就检查一下是不是在父pom的插件管理里配置了但子模块没有执行。Spring Boot多模块工程里父pom的pluginManagement只能统一版本不能自动让所有子模块执行repackage每个需要打成war的服务模块都要显式声明这个插件。还有一个隐蔽的问题common模块本身被错误地配置成了war。如果你在common的pom里也写了packagingwar/packaging并加了Spring Boot插件那它打出来的就是一个不能启动的“伪war”。更隐蔽的是有些公共代码里带SpringBootApplication会在服务启动时被扫描到导致Spring上下文被重复加载。遇到这种情况把common的packaging改回jar把启动相关注释和类清理掉再重新打包。验证公共模块是否进war可以执行unzip -l service-a.war | grep common如果能看到common-1.0-SNAPSHOT.jar说明依赖已经正确打进去了。如果看不到回头检查多模块打包命令和本地仓库里的common版本。4.4 日志去哪了Tomcat里不要按老思维找日志还有一个特别容易把人绕晕的点日志文件。jar方式启动时日志路径会按照你application.yml里的相对路径生成一般在项目工作目录或者指定的路径下。但war包部署到Tomcat后工作目录变成了Tomcat的bin目录如果你配置了相对路径日志会出现在你完全想不到的地方。我那次部署后服务一直显示启动成功但日志就是找不到。在service-a目录下找了半天最后发现日志输出到了Tomcat的logs目录下而且全部和Tomcat日志混在一起。排查起来特别费劲。我建议在war包部署场景下直接把日志路径配成绝对路径logging: file: path: /data/logs/service-a这样日志会固定输出到指定目录不管Tomcat从哪里启动都不会跑偏。如果你仍然想用相对路径至少要知道它默认落在Tomcat的bin目录下别按jar包部署的经验去找。另外Spring Boot应用的启动日志在解压后的logs目录里通常不会单独生成文件除非你显式配置了logging否则大部分输出都进了Tomcat的catalina.out。生产环境排查问题时第一件事先看catalina.out第二件事再看你配置的日志目录。5. 验证与收尾亲测成功的判断标准5.1 一套可复验的部署检查清单这次打war部署踩了这么多坑之后我自己总结了一套检查清单每次部署新服务都按这个过一遍服务模块pom里packaging是warspring-boot-starter-tomcat的scope是provided启动类继承了SpringBootServletInitializer并重写configure父工程根目录用-pl service-a -am打包而不是单独进子模块目录打包war包体积正常WEB-INF/lib下能看到common依赖和其他Spring依赖Tomcat版本和Spring Boot版本匹配Spring Boot 3.x用Tomcat 10Tomcat端口和server.port保持一致注册中心显示的端口和实际监听端口一致war包名符合预期访问路径和网关路由对得上日志路径配置为绝对路径启动后能立刻找到日志文件健康检查接口通过curl验证返回UP再放流量这套清单我打印出来贴在工位上连续几次部署都没有再出过之前那些低级问题。5.2 我实际踩过的坑和最终体会如果让我给这次打war部署经历总结一句那就是Spring Cloud微服务打成war部署到Tomcat技术上完全可行但并没有想象中那么简单。最大的成本不在“改pom”这一步而在部署环境、注册中心、contextPath、日志路径这些边缘细节。很多时候你以为配置改对了实际只有部署到目标环境跑起来才知道有没有问题。我最终采用的方案是每个服务一个Tomcat实例war包统一命名成ROOT从根本上绕开了contextPath的坑。如果你所在的环境不允许这样搞那就要特别注意注册中心上报的地址是否带contextPath提前把网关路由和Feign路径设计好。另外如果不是外部强制要求我个人还是倾向于用jar包方式部署Spring Cloud服务毕竟那才是Spring Boot的正常打开方式。但万一你躲不掉war部署按这篇里的步骤走至少能少踩一大半坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑