资讯动态

SpringBoot+Vue整合部署到外置Tomcat:从jar改war到避坑全指南

发布时间:2026/10/14 8:34:45 来源:尧图企业网站定制
简介这份文档面向正在使用SpringBoot与Vue前后端分离架构的Java Web开发者尤其是需要将项目部署到Windows环境下Tomcat服务器的技术人员。内容围绕无Nginx场景下的部署流程展开涵盖后端WAR包改造、前端dist资源整合、Tomcat的server.xml配置调整以及刷新页面404错误的web.xml处理方案并延伸至调用第三方API时的跨域配置思路。资源包共1个doc文件大小约72KB以文字步骤与配置代码片段为主便于对照操作。目前已有1450人学习下载适合具备一定SpringBoot与Vue基础、希望快速完成Tomcat部署并排查常见问题的开发者参考可帮助读者理清前后端资源在Tomcat中的路径映射关系减少部署过程中的试错成本。1. 把 SpringBoot Vue 塞进 Tomcat为什么前后端分离项目还要走这步很多人第一次听到“Tomcat 部署 SpringBoot Vue”第一反应是SpringBoot 不是自带内嵌 Tomcat 吗Vue 打完包不是扔 Nginx 就完事了吗为什么还要把两个东西合起来塞进一个外置 Tomcat这个问题我在实际交付里被问过不下十次。真实场景是这样的甲方运维体系里只有一套 Tomcat 集群安全策略、日志采集、端口开放、证书挂载全部围绕它做完了你临时申请一个 Nginx 或者新开一个内嵌端口流程能拖两周。这时候把 Vue 的静态产物和 SpringBoot 的接口打进同一个 war丢进既有 Tomcat是最省事的落地路径。这篇文章讲的就是这条路径怎么把 SpringBoot 从 jar 改成 war、Vue 的 dist 放哪、前端路由和后端接口怎么不打架、Tomcat 版本和 JDK 怎么配、以及上线后 404 和刷新白屏到底出在哪。适合手上只有 Tomcat 可用、又不想把前后端拆成两套部署体系的开发者。下面按我实际改过的一个模拟项目 X 的流程走一遍参数和坑都写清楚。2. SpringBoot 改 war 与 Tomcat 版本选型先定容器再动代码2.1 为什么外置 Tomcat 部署必须把 jar 改成 warSpringBoot 默认打成可执行 jar靠spring-boot-starter-web里内嵌的 Tomcat 启动main方法一跑容器就起来了。但外置 Tomcat 的启动入口是它自己的Bootstrap它不认识 SpringBoot 的main只认 Servlet 规范里的ServletContainerInitializer和WebApplicationInitializer。所以要让外置 Tomcat 接管必须做两件事把打包方式改成 war并让启动类继承SpringBootServletInitializer重写configure。这不是可选项是硬性前提。常见做法是在pom.xml里把spring-boot-starter-web的内嵌 Tomcat 标记为provided这样打出来的 war 里不会带 Tomcat 的 jar避免和外置容器的类冲突。我一般还会顺手把spring-boot-maven-plugin的mainClass保留方便本地还能用 jar 方式调试。!-- pom.xml 关键片段 -- packagingwar/packaging dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 关键外置 Tomcat 提供打包时排除防止类冲突 -- exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId !-- provided 表示编译期可见、运行期由容器提供 -- scopeprovided/scope /dependency /dependencies逻辑说明exclusions把内嵌 Tomcat 从 starter-web 里摘掉再用一条独立的provided依赖补回来。这样编译时能用到 Servlet API运行时由外置 Tomcat 提供实现。参数上唯一要注意的是scope必须是provided写成compile就会把 Tomcat 的 jar 打进 war部署时大概率报ClassNotFoundException或者LinkageError。2.2 启动类改造SpringBootServletInitializer 的写法启动类改造只有几行但写错位置会导致 Tomcat 启动时找不到 Spring 上下文表现为访问任何接口都是 404日志里却看不到明显异常。SpringBootApplication public class DemoApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 外置 Tomcat 通过这个入口加载 Spring 上下文 return builder.sources(DemoApplication.class); } }逻辑说明SpringBootServletInitializer实现了WebApplicationInitializerTomcat 启动时会扫描到它并调用configure。sources里传启动类等价于内嵌模式下的SpringApplication.run。参数上没什么可调的但要注意如果项目里有多个Configuration入口别在这里重复注册否则会出现 Bean 重复定义的启动失败。2.3 Tomcat 版本和 JDK 的对应关系版本选错是部署阶段最隐蔽的坑。Tomcat 9 对应 Servlet 4.0Tomcat 10 开始把javax.servlet换成了jakarta.servlet这个包名变更会让所有基于javax的 SpringBoot 2.x 项目直接启动失败。我一般按下面这张表来定Tomcat 版本Servlet 规范包名适配的 SpringBootTomcat 8.5Servlet 3.1javaxSpringBoot 1.x / 2.xTomcat 9Servlet 4.0javaxSpringBoot 2.xTomcat 10.0Servlet 5.0jakartaSpringBoot 2.7 需迁移Tomcat 10.1Servlet 6.0jakartaSpringBoot 3.x选型建议如果项目是 SpringBoot 2.x直接锁 Tomcat 9别碰 Tomcat 10。如果非要用 Tomcat 10要么把 SpringBoot 升到 3.x要么做javax到jakarta的包名迁移工作量不小。JDK 方面Tomcat 9 建议 JDK 8 或 11Tomcat 10.1 建议 JDK 17。我见过有人在 JDK 17 上跑 Tomcat 9 加 SpringBoot 2.3编译能过运行时报模块访问异常最后降回 JDK 11 才稳。3. Vue 打包产物与 SpringBoot 静态资源整合路径和路由是重灾区3.1 Vue 的 dist 到底放哪三种放法和各自代价Vue 打完包是一堆静态文件要让它被 SpringBoot 的 war 一起带进 Tomcat常见有三种放法。第一种是放进src/main/resources/static打包时自动进WEB-INF/classes/staticSpringBoot 默认就会把它当静态资源目录。第二种是放进src/main/webapp打包后位于 war 根目录Tomcat 默认的default servlet能直接服务。第三种是单独配ResourceHandler指向外部目录适合静态文件频繁更新、不想重新打包的场景。我一般选第一种因为路径可控、和 SpringBoot 的静态资源机制天然契合。但要注意static目录下的文件会被 SpringBoot 的ResourceHttpRequestHandler接管如果前端路由用了 history 模式刷新非根路径会 404这个后面单独讲。# Vue 项目打包输出到 dist npm run build # 把 dist 内容拷进 SpringBoot 的 static 目录 # 注意是拷 dist 里面的内容不是拷 dist 这个文件夹本身 cp -r dist/* /path/to/springboot-project/src/main/resources/static/逻辑说明npm run build的产物默认在dist里面通常有index.html、js/、css/、img/等。拷的时候要拷内容如果拷成static/dist/index.html访问路径就变成了/dist/index.html和预期不符。参数上Vue 的vue.config.js里publicPath要设成./或/设成/dist/会导致所有资源引用路径多一层页面白屏。3.2 前端路由 history 模式与后端接口的路径冲突Vue Router 默认是 hash 模式URL 带#刷新不会打到后端所以不会 404。但很多人为了 URL 好看改成 history 模式这时候刷新/user/list这种路径浏览器会直接向 Tomcat 请求/user/listTomcat 找不到对应资源就返回 404。解决办法是在后端加一个转发规则把所有非接口、非静态资源的请求转发到index.html。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 静态资源走默认的 classpath:/static/ registry.addResourceHandler(/**) .addResourceLocations(classpath:/static/); } Override public void addViewControllers(ViewControllerRegistry registry) { // 把非 /api 开头的无后缀路径转发到 index.html解决 history 刷新 404 registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); } }逻辑说明addViewControllers里的正则[^\\.]*表示匹配不含点的路径这样/user/list会被转发到index.html而/js/app.js这种带点的静态资源不会被误转发。参数上如果接口路径也用了无后缀形式比如/api/user要确保它不被这条规则拦截通常做法是接口统一加/api前缀然后在正则里排除或者把接口路径放在更具体的RequestMapping里Spring 的匹配优先级会先命中控制器。3.3 接口前缀与跨域同源部署后 CORS 配置要清理前后端分离开发时Vue 的devServer配了 proxy后端也开了 CORS。但合并到同一个 war 部署后前端和后端同源了原来的 CORS 配置反而可能引发问题比如响应头里出现重复的Access-Control-Allow-Origin某些浏览器会直接拒绝。我一般在上线前把CrossOrigin注解和全局 CORS 配置都去掉接口前缀统一成/api前端 axios 的baseURL也改成/api。// 前端 axios 配置同源部署后 baseURL 用相对路径 const service axios.create({ baseURL: /api, // 不再写完整域名避免跨域 timeout: 10000 })逻辑说明baseURL用/api是相对路径请求会打到当前域名下的/api和 war 的 context path 拼接后就是最终地址。参数上要注意如果 Tomcat 里 war 的 context path 不是根路径比如/demo那baseURL要么写成/demo/api要么用./api相对当前路径。我一般建议 war 包名和 context path 保持一致减少心智负担。4. 部署实操从打 war 到 Tomcat 跑起来的完整命令链4.1 打 war 包并检查产物结构打包命令本身简单但产物结构必须检查否则部署后各种找不到类、找不到资源。# 在 SpringBoot 项目根目录执行 mvn clean package -DskipTests # 检查 war 包结构确认静态资源和类都在正确位置 unzip -l target/demo-0.0.1-SNAPSHOT.war | head -30逻辑说明-DskipTests跳过测试加快打包正式发版建议保留测试。unzip -l列出 war 内容重点看三处WEB-INF/classes/static/index.html是否存在WEB-INF/lib/下有没有 Tomcat 的 jar有就说明provided没生效WEB-INF/classes/下启动类是否编译进去。参数上war 包名就是默认的 context pathdemo-0.0.1-SNAPSHOT.war部署后访问路径是/demo-0.0.1-SNAPSHOT/嫌长可以在pom.xml的finalName里改成demo。4.2 部署到 Tomcat 并验证启动把 war 丢进 Tomcat 的webapps目录启动后看日志。这一步的排查信息全在日志里别急着开浏览器。# 拷贝 war 到 Tomcat webapps cp target/demo.war $TOMCAT_HOME/webapps/ # 启动 TomcatLinux 下 $TOMCAT_HOME/bin/startup.sh # 实时看启动日志重点找 Started 和 Exception tail -f $TOMCAT_HOME/logs/catalina.out逻辑说明Tomcat 启动时会自动解压 war 到同名目录日志里出现Spring Boot的 banner 和Started DemoApplication说明 Spring 上下文加载成功。如果只看到 Tomcat 启动但没看到 Spring 日志多半是SpringBootServletInitializer没生效。参数上catalina.out是主日志localhost.日期.log里会有更细的 Servlet 容器异常两个都要看。4.3 验证静态资源和接口是否都能访问启动成功后按顺序验证先访问根路径看index.html是否返回再访问一个静态资源看是否 200最后访问一个接口看数据是否正常。# 验证首页 curl -I http://localhost:8080/demo/ # 验证静态资源 curl -I http://localhost:8080/demo/js/app.js # 验证接口 curl http://localhost:8080/demo/api/user/list逻辑说明-I只看响应头200说明资源可达404说明路径不对500说明后端异常。接口返回数据说明 Spring 上下文和数据库连接都正常。参数上如果首页返回 404 但静态资源 200说明index.html没被正确转发检查addViewControllers配置如果接口 404 但首页正常检查RequestMapping的路径和 context path 是否拼接正确。5. 避坑与排查Tomcat 部署前后端项目的五个高频翻车点5.1 启动报 javax.servlet 相关 ClassNotFound现象Tomcat 启动日志里出现java.lang.NoClassDefFoundError: javax/servlet/ServletContext或类似异常应用无法启动。原因war 包里打进了 Tomcat 的 jar和外置容器的类加载器冲突或者 Tomcat 10 用了jakarta包名而项目还是javax。解决检查pom.xml里spring-boot-starter-tomcat的scope是否为provided用unzip -l确认 war 里没有tomcat-embed-core如果是 Tomcat 10降回 Tomcat 9 或迁移包名。5.2 页面白屏但接口正常现象访问首页返回 200但浏览器一片空白控制台报资源 404。原因Vue 的publicPath配错打包后index.html里引用的 js/css 路径多了或少了层级。解决检查vue.config.js的publicPath同源部署一般设./或/重新打包后看index.html里的script src路径是否和实际访问路径一致。5.3 刷新非根路径 404现象首页能打开点导航也能跳但按 F5 刷新/user/list就 404。原因history 模式下浏览器直接请求该路径Tomcat 找不到对应资源。解决加addViewControllers转发规则把无后缀路径转发到index.html同时确保接口路径不被误转发。5.4 接口返回 404 但本地 jar 模式正常现象本地用java -jar跑一切正常部署到 Tomcat 后接口全 404。原因war 部署后 context path 变了前端请求路径没跟着改或者server.servlet.context-path配置和外置 Tomcat 的 context path 冲突。解决确认 war 包名和访问路径前端baseURL加上 context path如果application.yml里配了context-path外置 Tomcat 下这个配置不生效要以 war 包名为准。5.5 静态资源缓存导致更新不生效现象重新部署后页面还是旧版本清浏览器缓存才好。原因Tomcat 对静态资源默认开了缓存或者 Vue 打包的文件名没带 hash。解决Vue 打包默认会给 js/css 加 hash如果没加在vue.config.js里配filenameHashing: trueTomcat 侧可以在web.xml里给静态资源加Cache-Control头或者用版本号查询参数强制刷新。6. 进阶技巧用 context path 隔离多环境与灰度验证前面讲的都是单 war 部署实际项目里经常遇到同一台 Tomcat 要跑多个环境或者要做灰度验证。这时候 context path 的用法就值得单独说一下。Tomcat 的 context path 由 war 包名决定demo.war对应/demodemo-test.war对应/demo-test。利用这个机制可以把测试环境和生产环境部署在同一台 Tomcat 上前端打包时通过环境变量注入不同的baseURL一套代码出两个 war。# 前端按环境打包注入不同的 API 前缀 # .env.test 里写 VUE_APP_BASE_API/demo-test/api # .env.prod 里写 VUE_APP_BASE_API/demo/api npm run build -- --mode test mv dist demo-test cp -r demo-test $TOMCAT_HOME/webapps/demo-test/逻辑说明Vue CLI 支持--mode指定环境文件VUE_APP_前缀的变量会注入到前端代码里。打包后把dist重命名成和 war 包名一致的目录Tomcat 会把它当成一个独立的 context。参数上要注意两个环境的 war 包名不能相同否则后部署的会覆盖先部署的数据库连接等后端配置通过application-{profile}.yml区分启动时用-Dspring.profiles.active指定。灰度验证的思路类似新版本部署成demo-gray.war前端灰度入口指向/demo-gray/api验证通过后再把demo.war替换掉。这样不用停机也不用改 Tomcat 配置。我一般会在灰度 war 的application.yml里把日志级别调成 DEBUG方便观察新代码的行为验证完再改回 INFO。最后说个我自己的习惯每次改完pom.xml或vue.config.js一定先本地mvn clean package打一次 war用unzip -l确认结构再往 Tomcat 上丢。这个动作花不了一分钟但能挡掉八成部署阶段的玄学问题。另一个习惯是 Tomcat 的catalina.out永远开着tail -f启动失败的第一现场全在那里比事后翻日志快得多。这套流程我用了几年从 SpringBoot 2.1 到 2.7、Tomcat 8.5 到 9 都跑通过版本升级时主要盯包名和 scope 两个点基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑