资讯详情

资讯详情

Vue+SpringBoot部署Tomcat:war 404、白屏与刷新404排查

简介面向SpringBoot与Vue前后端分离项目开发者的一份Tomcat部署文档适合需要将本地联调完成的应用迁移到Windows服务器、又暂不引入Nginx的团队参考。压缩包内仅含1个doc文件大小约72KB以文字说明集中整理后端WAR包改造、前端dist资源放置、Tomcat的server.xml中Context路径配置、刷新404处理及web.xml错误页设置等关键环节便于按章节查阅和团队传阅。文档还涉及调用百度地图等第三方API时的跨域配置思路并对登录后空白页、刷新报错、跨上下文访问等常见现象给出排查方向适合作为部署前的检查清单与问题对照材料。目前已有1458人学习内容偏实操记录与个人经验整理适合初中级Java Web开发者、运维人员及课程实训场景快速理解传统Tomcat部署前后端分离项目的完整脉络与易错点。1. 部署没报错就成功的情况很少war 丢进去 404、登录后白屏、刷新又 404前后端分离的项目在本机跑得好好的一挪到 Windows Tomcat 上常见的问题就集中爆发了war 包扔进 webapps 访问 404、能登录但进去一片空白、路由点一下刷新又 404。这三个现象在搜索结果里能找到一堆互相矛盾的答案有人让你改 server.xml有人让你加 web.xml还有人劝你干脆换成 hash 路由。我见过最折腾的一次几个人对着同一份部署文档改了两天最后发现是 war 包名和 webapps 下的解压目录对不上。这篇笔记就沿着这条链路把 vue tomcat 部署 springboot 过程中每个报错拆开它为什么发生、用日志和浏览器怎么确认、改哪一行能解决以及下次怎么少走一遍。适合手里只有一台 Windows 服务器和一个 Tomcat、又要接手老项目部署的开发者。2. war 包丢进 webapps 却 404从 pom 到 context 路径的完整链路把 SpringBoot 项目改成 war 部署到外部 Tomcat看起来只是改一行packaging实际上有三处必须同时成立依赖范围对了、启动类被容器接管了、war 名和访问路径对上了。任何一处漏掉表现都是同一个——页面 404日志里却风平浪静这是最容易让人怀疑人生的地方。2.1 只把 packaging 改成 war 还不够ServletInitializer 与 provided 依赖先把 pom 改对。很多人只改打包方式结果 war 包能部署、Tomcat 也能起来但所有接口全是 404因为 Spring 的 DispatcherServlet 压根没注册到外部容器里。同时内嵌 Tomcat 的依赖必须调成 provided否则 war 包里带一份 Tomcat外部容器再加载一份类冲突是迟早的事。!-- pom.xml打包方式 容器依赖范围 -- packagingwar/packaging dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 关键一步内嵌 Tomcat 只在编译和本地调试时可见不打进 war -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId scopeprovided/scope /dependencyprovided的含义是「编译期和测试期可用运行期由环境提供」。外部 Tomcat 就是那个环境它自带 servlet 容器所以 war 里不需要再带一份。副作用是本地直接java -jar跑不起来了常见做法是在 IDE 里跑 main 方法IDE 的运行时 classpath 仍然包含 provided 依赖或者临时切回默认 scope 调试。改完依赖还要补一个初始化类把 Spring 容器的启动权交给外部容器。// 名字随意继承 SpringBootServletInitializer 即可 public class TomcatServletInitializer extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 指向原来的启动类让外部 Tomcat 触发 Spring 容器初始化 return builder.sources(Application.class); } }这个类的原理不复杂外部 Tomcat 启动时会通过 SPI 找到SpringServletContainerInitializer再由它回调所有WebApplicationInitializer的实现。SpringBootServletInitializer就是这个接口的实现它会替你创建AnnotationConfigWebApplicationContext并注册 DispatcherServlet。少了它war 只是个普通的空壳 webapp访问任何接口都是 404但 Tomcat 的 catalina 日志里不会报任何错这是最典型的「没报错不等于部署成功」。打包命令建议加跳过测试很多项目在服务器上构建时因为测试用例要连数据库直接失败。# 在项目根目录执行跳过测试避免构建期连库失败 mvn clean package -DskipTests # 确认产物名字和大小war 里不该出现 tomcat-embed 的 jar ls -lh target/*.war unzip -l target/prod-api.war | grep -i tomcat如果unzip -l里还能看到tomcat-embed-core说明provided没生效大概率是父 pom 或别的 starter 又把它传递进来了用mvn dependency:tree -Dincludesorg.apache.tomcat.embed定位来源再排除。2.2 启动日志没报错但页面 404war 名、端口、context 三处对齐第二个高频原因是访问路径没对上。war 包放进 webapps 之后Tomcat 默认用文件名作为 context pathprod-api.war对应的访问前缀就是/prod-api接口地址是http://ip:8080/prod-api/xxx不是http://ip:8080/xxx。文档里让你把 war 重命名为prod-api本质上就是在定这个前缀。这里有个特别容易翻车的点在外部容器部署模式下application.yml里的server.port和server.servlet.context-path全部失效。端口由 Tomcat 的conf/server.xml里 Connector 的port决定context path 由 war 包名决定。我见过有人在 yml 里写server.port9090然后对着 8080 一直刷 404查了两小时配置。判断方法很直接看 Tomcat 启动日志最后几行有没有Deployment of web application archive [...] has finished in [x] ms以及Server startup in [x] milliseconds再对照localhost_access_log里的实际请求路径。改完包名以后还有一件事必须做——清干净旧产物。Tomcat 的autoDeploy会同时看prod-api.war和它解压出来的prod-api/目录如果只替换 war 不删目录很可能跑的还是旧代码。# 停掉 Tomcat 之后执行三个目录都要清 rm -rf webapps/prod-api webapps/prod-api.war rm -rf work/Catalina/localhost/prod-api # 再放新 war 启动强制重新解压work目录存的是 Tomcat 编译后的 JSP 和会话相关缓存删掉不影响业务数据但会清掉所有会话登录态需要重新来一次。生产环境操作前先确认没人正在用。2.3 内嵌 Tomcat 与容器 Tomcat 打架的几种典型表现依赖没调成 provided 时报错通常长这样java.lang.ClassCastException、java.lang.NoSuchMethodError、NoClassDefFoundError: javax/servlet/ServletContext或者应用被初始化了两次、日志里出现两个不同版本的 Tomcat 版本号。根因是类加载器隔离被打破同一个类被容器和 war 各加载一份instanceof判断直接失效。还有一类更隐蔽的Servlet API 的包名不匹配。SpringBoot 3.x 起全面切到jakarta.servlet.*对应的容器要求 Tomcat 10 及以上把这样的 war 丢进 Tomcat 8.5 或 9启动时就报NoClassDefFoundError: javax/servlet/http/HttpServlet或ClassNotFoundException: jakarta.servlet.ServletContext。这不是配置问题是版本不匹配只能二选一降框架版本到 2.7.x或者把 Tomcat 升到 10。别在这上面浪费时间去改 server.xml那是南辕北辙。判断顺序我一般是这样先看logs/catalina.out里的第一个异常不是最后一个最后一个往往是级联失败再看 war 包里有没有内嵌容器最后核对 Tomcat 主版本和 SpringBoot 主版本的对应关系。三步之内基本能定位。3. 登录能进但一片空白Vue history 路由撞上 Tomcat 默认站点白屏是这类部署里最让人迷惑的现象登录接口明明通了token 也存下了路由跳转之后页面就是一片白控制台有时连红色报错都没有。它和 404 不是一回事但根因往往在同一个地方——服务端不认识前端路由。3.1 先分清是白屏还是 404Network 面板三秒判定打开浏览器开发者工具的 Network 面板勾上 Disable cache 再刷新看两件事文档请求返回的状态码以及js、css这些静态资源是不是 200。如果是 404且请求路径长得像/dashboard、/user/list这种业务路由说明前端用的是 history 模式浏览器把路由地址原样发给了 TomcatTomcat 按物理目录找不到对应的文件夹直接 404。如果文档请求是 200、HTML 也返回了但static/js/index.xxxx.js是 404那是资源基准路径publicPath的问题页面骨架在脚本没加载同样是一片白。还有一种情况所有资源都 200但 Vue 的 router 匹配不到任何路由控制台会出现No match found for location with path /xxx的警告这说明前端路由的 base 配置和服务端部署路径没对上。我一般还会顺手看一眼 Elements 面板如果#app里是空的说明 Vue 挂载失败或路由没渲染如果#app里内容在、只是样式全丢那就是 css 路径错了。这个区分很关键三种情况对应的修法完全不同。3.2 server.xml 里 Context 的写法与三个参数的真实含义history 模式的正解是让 Tomcat 在找不到物理路径时把请求交给index.html处理。文档里给的做法是在conf/server.xml的Host节点内加一个Context把前端dist目录挂成根应用!-- conf/server.xmlHost 节点内部 -- Host namelocalhost appBasewebapps unpackWARstrue autoDeploytrue !-- 前端 dist 挂在根路径后端 war 用 prod-api 前缀 -- Context path docBaseD:/deploy/dist reloadablefalse crossContexttrue / /Host三个属性值得说清楚。path是 URL 前缀写空字符串表示根上下文访问http://ip:8080/就是这个应用写成/在多数版本也能访问但部分 Tomcat 会对根上下文的写法给出告警空字符串更稳。docBase指向前端构建产物所在的物理目录可以是绝对路径也可以是相对webapps的路径。reloadable控制类文件变化时是否自动重载开发环境设true方便生产环境建议false因为它会周期性扫描 class 文件有额外开销。至于crossContext它的作用是允许通过ServletContext.getContext()访问同一虚拟主机下其他应用的上下文跟路由白屏没有直接关系——这个属性在原始文档里也是照抄过来的属于典型的「加了没坏处但也没解决问题」的配置真正的解药是让 404 回退到index.html那是下一章的事。还有个细节server.xml只在启动时读取一次改完必须重启 Tomcat热改不生效。另外从 Tomcat 8 开始官方更推荐把 Context 拆到conf/Catalina/localhost/下按应用名建 xml 文件避免server.xml被反复改动。写在server.xml里依然能用只是升级或迁移时容易丢我自己维护的项目一律用独立 xml 文件。3.3 docBase 写绝对路径还是相对路径一个换机器就崩的细节绝对路径最直观D:/deploy/dist指哪打哪但它绑死了机器和目录结构换台服务器就要改配置。相对路径是相对webapps目录的比如dist就表示webapps/dist迁移时只要目录结构一致就能直接跑。代价是相对路径依赖启动目录如果 Tomcat 被注册成 Windows 服务或者用快捷方式启动工作目录可能不是bin相对路径就会解析到别的地方去表现为「昨天还好好的今天起来就 404」。我踩过这个坑后来统一成绝对路径并且在部署脚本里把路径写死成一个变量改配置只改一处。Windows 下路径分隔符也容易出问题。server.xml是 XML反斜杠是转义字符docBaseD:\deploy\dist会被解析成奇怪的路径。两种写法都安全用正斜杠D:/deploy/dist或者双反斜杠D:\\deploy\\dist。路径里带空格或中文时务必用英文目录Tomcat 对非 ASCII 路径的处理在不同版本上并不一致。配好之后验证也简单重启 Tomcat在浏览器直接访问http://ip:8080/能看到登录页说明 Context 生效再访问一个不存在的路由地址如果还是 404说明回退规则没配上接着往下走。4. 刷新页面就 404WEB-INF/web.xml 兜底与静态资源路径Context 配好之后从首页点进各个菜单都正常但只要在任意一个内页按 F5立刻 404。这不是配置没生效而是刷新这个动作把请求直接发给了服务端而服务端确实没有这个物理路径。4.1 error-page 404 回退到 index.html这段 web.xml 到底做了什么解决办法是在dist目录下建一个WEB-INF文件夹里面放web.xml声明 404 错误页指向index.html!-- dist/WEB-INF/web.xml404 统一回退到首页 -- ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 metadata-completetrue display-nameRouter for Tomcat/display-name error-page error-code404/error-code location/index.html/location /error-page /web-app原理是 Tomcat 的ErrorReportValve在处理响应时如果发现状态码是 404会去查当前 Web 应用的error-page配置命中就内部转发到location指定的资源。/index.html是相对当前 context 根的绝对路径所以前端挂在根上下文时写/index.html就行如果前端挂在/prod-api下就写/prod-api/index.html。顺带说一句WEB-INF目录的存在本身还有一层意义Tomcat 判断一个目录是不是 Web 应用看的就是有没有WEB-INF。dist里加了WEB-INF/web.xml它才是一个完整的 webapp 目录docBase指过去才能按应用方式加载。metadata-completetrue的作用是告诉容器不用扫描注解纯静态站点没有注解可扫加上能省一点启动时间。配完记得重启 Tomcat并且确认改的是部署目录下的那份WEB-INF不是构建产物源目录里已经过期的副本——重新npm run build会清空dist这个文件会被删掉。常见做法是把它放到public/WEB-INF/web.xml让构建时自动带进dist。4.2 静态资源 404base、publicPath 与 dist 的目录层级回退配好之后还有一种顽固的 404HTML 回来了但页面还是白Network 里static/js/app.xxxx.js是红色。原因在于index.html里引用资源用的是相对路径或错的基准路径。如果路由是/user/list浏览器会以/user/为基准去解析./static/js/app.js实际请求变成/user/static/js/app.js当然 404。修法是在构建配置里把publicPath定成绝对路径// vue.config.js module.exports { // 部署在根路径用 /部署在子路径如 /prod-api/必须换成 /prod-api/ publicPath: /, outputDir: dist, // 生产环境不生成 source map减小 dist 体积 productionSourceMap: false, devServer: { port: 8081 } }publicPath决定index.html中所有资源引用的前缀。前端挂根路径就写/挂在/prod-api/子路径就写/prod-api/结尾的斜杠不能省。这个值配错的表现非常典型首页能开一点菜单就白因为深层路由下的相对路径解析全歪了。还有一个更省事的选项——把 vue-router 切到 hash 模式。#后面的部分不会发给服务端刷新永远不会 404也就不需要web.xml兜底。代价是 URL 里带#如果对方对地址栏形态有要求那就只能老老实实配 history 回退。4.3 接口 404 与 /prod-api 前缀axios baseURL 与后端 context-path 对齐前端页面正常之后下一个报错通常来自接口登录点不动Network 里POST /prod-api/login返回 404或者返回的是index.html的内容因为 404 被回退成首页了前端 JSON 解析失败报Unexpected token 。这个报错很有辨识度——只要解析报错里出现基本可以确定请求打到了 HTML 上。根因是请求路径拼错。前端在根上下文/后端 war 是prod-api接口的真实地址是/prod-api/login。axios 的baseURL必须与之对齐// request.js接口前缀要和 war 包名保持一致 const service axios.create({ baseURL: process.env.NODE_ENV production ? /prod-api : /api, timeout: 10000 })这里baseURL结尾不能带斜杠请求路径开头要带斜杠否则会拼成/prod-api//login某些容器会返回 404。开发环境用/api配合 devServer 的 proxy 转发到本地后端生产环境直接用 war 名这样一套代码两边都能跑。如果后端在application.yml里还配了server.servlet.context-path注意它在外部 Tomcat 下不生效真正生效的是 war 名别把两者混着算。5. 常见问题排查从端口占用到第三方 API 跨域的六条现场记录下面这几条是我在不同项目里反复遇到的每条都按「现象 → 原因 → 解决」写遇到类似症状可以直接对号入座。5.1 部署阶段的三条旧目录、端口、没重启第一条war 换了新的跑起来还是旧代码。现象是明明重新打包上传了页面上的改动一点没生效日志里也没有重新部署的记录。原因是 webapps 下同时存在旧 war 和旧的解压目录Tomcat 的 autoDeploy 判定不需要重新解压。解决方式是停服后删掉webapps/prod-api、webapps/prod-api.war、work/Catalina/localhost/prod-api三处再放新包启动日志里出现Deployment of web application archive才算真的换了。第二条启动直接失败日志报java.net.BindException: Address already in use: bind。现象是 Tomcat 窗口一闪而过或者卡在启动阶段。原因是 8080 端口被别的进程占了常见的是上一次的 Tomcat 没退干净或者本地还跑着一个前端 devServer。# Windows 下查占用并强制结束 netstat -ano | findstr :8080 taskkill /PID 12345 /Fnetstat输出最后一列就是 PID拿到直接 kill。如果杀不掉多半是权限不够用管理员身份开命令行。另一个选择是改server.xml里 Connector 的port但改完记得同步前端接口地址和防火墙规则。第三条改了 server.xml 完全没反应。现象是 Context 加进去了访问还是 404配置像是没读。原因是 Tomcat 只在启动时解析server.xml运行期修改无效还有一种可能是改错了文件目录里躺着server.xml.bak、server.xml.20230101之类的备份编辑器打开的是备份。解决方式是确认改的是conf/server.xml保存后完整重启不是 reload再看启动日志里有没有 XML 解析错误——格式写错时 Tomcat 会直接启动失败而不是忽略配置。5.2 联调阶段的三条跨域、白名单、Cookie 路径第四条调用第三方 API 报 CORS 错误。现象是控制台出现Access to XMLHttpRequest at https://third-party.example.com/xxx from origin http://192.168.1.10:8080 has been blocked by CORS policy接口在 Postman 里却是通的。原因是 CORS 是浏览器强制的同源策略Postman 不发预检、不校验响应头所以看起来正常。第三方服务没有把你的新域名或 IP 端口加进允许列表浏览器就直接拦了。解决思路两条。优先走后端代理让请求从服务器发出天然没有同源限制RestController RequestMapping(/api/proxy) public class ThirdPartyProxyController { private final RestTemplate restTemplate new RestTemplate(); GetMapping(/geocode) public String geocode(RequestParam String address) { // 前端只调自己的域名第三方请求由后端发起绕开浏览器同源策略 String url https://third-party.example.com/geocode?addr address; return restTemplate.getForObject(url, String.class); } }RestTemplate在这里只做转发超时和重试要显式配置默认没有超时会导致线程堆积。如果确实需要浏览器直连那就得在服务端加 CORS 响应头Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration cfg new CorsConfiguration(); // 只放行自己前端所在的实际地址不要图省事写 * cfg.addAllowedOrigin(http://192.168.1.10:8080); cfg.addAllowedHeader(*); cfg.addAllowedMethod(*); // 需要携带 Cookie 时必须为 true此时 Origin 不能是 * cfg.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, cfg); return new CorsFilter(source); } }这里有个组合限制allowCredentials(true)和allowedOrigin(*)不能同时使用浏览器会直接拒绝。上线前把 Origin 写死成实际域名别用通配。第五条第三方服务返回域名或 Referer 校验失败。现象是请求发出去了返回 403 或者一个INVALID_USER_DOMAIN之类的错误码地图不显示、组件空白。原因是这类服务通常要求把调用方域名加进控制台白名单而你现在用的是IP:端口或者刚换的新域名还没登记。解决方式是登录对应平台把新地址加进白名单并把 Referer 策略调整为允许发送如果对方只认备案域名那就只能用后端代理把请求收敛到自己的域名下。第六条登录成功但后续接口全 401。现象是登录接口返回了 token 或写了 Cookie紧接着的请求却提示未授权。原因是 Cookie 的path被设成了/prod-api而前端页面在根路径/浏览器按路径匹配不会带上这个 Cookie。解决方式是在服务端把 Cookie 的 path 设为/或者干脆改用 Header 里带 token 的方案避开 Cookie 路径和SameSite这一堆边界情况。前后端分离项目里后者更省心。6. 快速定位五分钟判断问题出在打包、Tomcat 还是前端路由排这类故障最怕的是没有顺序地乱试。我的习惯是先做一次分层判定把范围从「整个系统」压到「某一层」再往下钻。第一步看 Tomcat 有没有真的把应用起来。打开logs/catalina.out搜索Server startup in和Deployment of web application archive两条都在说明容器层没问题只有前者没有后者说明 war 没被识别回去查包名和 webapps 目录权限。第二步用 curl 直接打后端接口绕开浏览器# 直接验证后端是否可用不走前端 curl -i http://127.0.0.1:8080/prod-api/actuator/health # 验证前端静态页是否被正确返回 curl -i http://127.0.0.1:8080/index.html接口通、页面不通问题在前端两个都不通问题在容器或路径。第三步用浏览器 Network 面板区分白屏类型对照下表现象先看哪里快速动作访问根路径直接 404webapps 下有没有解压目录看 catalina.out 的部署日志登录后白屏js/css 报 404publicPath 与 Context 的 path改成绝对路径重新构建内页刷新 404dist 下有没有 WEB-INF/web.xml补文件并重启 Tomcat接口 404 且返回内容是 HTMLaxios baseURL 与 war 名对齐/prod-api前缀控制台 CORS 报错请求目标是不是第三方域名后端代理或加白名单启动报 NoClassDefFoundErrorSpringBoot 与 Tomcat 主版本对齐 jakarta / javax第四步才是改配置。改完必须重启 Tomcat 再验一次别在浏览器里反复刷新等它自己生效——server.xml和web.xml都不是热加载的。血泪经验是这套流程里最容易跳过的是第一步大家习惯直接盯着浏览器报错改前端结果改了半天发现 war 根本没部署上去。从那以后我每次部署完都强制先跑一遍curl接口和curl首页这两条命令确认服务端这一层是干净的再去动前端配置。这份部署文档里把 pom 改造、war 命名、Context 配置、web.xml 兜底和跨域处理这几步都串起来了配合上面的排查顺序用能省掉大部分来回试错的时间。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →