ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tomcat部署SpringBoot+Vue:白屏与404排查实战

Tomcat部署SpringBoot+Vue:白屏与404排查实战

简介:这份文档面向Java Web开发者与运维人员,聚焦SpringBoot+Vue前后端分离项目在Tomcat上的部署实践,帮助解决从本地运行到服务器上线过程中的打包、路径配置与跨域等常见问题。资源包共1个doc文件,大小约72KB,内容以文字步骤与配置代码为主,便于对照操作。文档围绕WAR包改造、Vue构建产物放置、server.xml中Context节点配置、404刷新处理以及第三方API跨域配置等环节展开,给出了可直接参考的代码片段与排错思路,适合需要将前后端分离项目部署到Windows+Tomcat环境的读者查阅。目前已有1450人学习下载,可作为部署流程的速查手册使用。

1. 为什么 war 包丢进 webapps 还是白屏:一次 Tomcat 部署的完整复盘

把 SpringBoot 打成 war 丢进 Tomcat 的 webapps,再把 Vue 的 dist 拷进去,浏览器一开——登录页出来了,输完账号密码,白屏。这个场景我在不同项目里见过太多次,每次排查方向都不一样,但根子往往就一个:前端路由和后端上下文没对齐。这份文档记录的就是 Windows + Tomcat 环境下,不借助 Nginx,纯靠 Tomcat 把 SpringBoot + Vue 前后端分离项目跑起来的完整流程。它适合手头只有一台 Windows 服务器、不想额外装 Nginx、又必须把项目部署上线的后端或全栈同学。文档里提到的prod-api命名、server.xml的 Context 配置、WEB-INF/web.xml的 404 兜底,都是实际部署中绕不开的环节。下面按「打包 → 放置 → 配置 → 排错」的顺序拆一遍,中间会重点说清楚刷新 404 和跨域这两个最容易翻车的地方。

2. 从 jar 到 war:SpringBoot 打包方式切换与 Tomcat 目录规划

2.1 为什么必须改 packaging,以及改完之后 pom 里还要动什么

SpringBoot 默认用内嵌 Tomcat 跑 jar,java -jar一条命令就完事。但一旦决定用外部 Tomcat 部署,第一步就是把pom.xml里的<packaging>jar</packaging>改成<packaging>war</packaging>。这个改动本身不复杂,但改完之后如果直接mvn package,打出来的 war 包丢进 Tomcat 大概率起不来,因为内嵌的 Tomcat 依赖还在,和外部的 Tomcat 容器冲突。

常见做法是在spring-boot-starter-web依赖里把内嵌 Tomcat 排除掉,同时补上spring-boot-starter-tomcat并标记为provided。这样打包时不会把 Tomcat 的 jar 塞进 war 的WEB-INF/lib里,运行时由外部 Tomcat 提供容器能力。

<!-- pom.xml 关键改动 --> <packaging>war</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!-- 排除内嵌 Tomcat,避免和外部 Tomcat 冲突 --> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 以 provided 方式引入,编译时需要,打包时不放入 lib --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> </dependencies>

参数说明:<scope>provided</scope>表示这个依赖只在编译和测试阶段有效,打包时不会进入WEB-INF/lib。如果漏了这一步,war 包里会同时存在内嵌 Tomcat 的类和外部 Tomcat 的类,启动时经常报ClassNotFoundException或者端口冲突。

另外,启动类需要继承SpringBootServletInitializer并重写configure方法,否则外部 Tomcat 找不到 SpringBoot 的入口。

// 启动类改造,让外部 Tomcat 能识别 SpringBoot 应用 @SpringBootApplication public class Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { // 指向本来的启动类,Tomcat 启动时会调用这里 return builder.sources(Application.class); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

逻辑说明:SpringBootServletInitializer是 SpringBoot 专门为 war 部署提供的适配器。外部 Tomcat 启动时通过 SPI 机制找到它,再调用configure方法加载应用上下文。不继承这个类,Tomcat 启动日志里只会看到 war 被解压,但 Spring 容器根本没起来。

2.2 打包命令、产物命名与 webapps 目录的放置策略

改完 pom 和启动类之后,执行打包命令。Maven 和 Gradle 的写法不同,这里以 Maven 为例:

# 在项目根目录执行,跳过测试加快打包速度 mvn clean package -DskipTests # 打包完成后,target 目录下会生成 war 文件 # 默认名称通常是 artifactId-version.war,比如 prod-api-0.0.1-SNAPSHOT.war

打包完成后,把 war 文件复制到 Tomcat 的webapps目录下,并重命名为prod-api.war。重命名这一步不是必须的,但建议做,因为 Tomcat 会用 war 文件名作为上下文路径。如果保持默认的prod-api-0.0.1-SNAPSHOT.war,访问路径就变成ip:端口/prod-api-0.0.1-SNAPSHOT/...,前端配置接口地址时容易写错。

Vue 项目这边,执行npm run build生成dist目录。dist里的内容不需要打成 war,直接整个文件夹复制到webapps下即可。最终webapps目录结构大致是这样:

webapps/ ├── prod-api/ # SpringBoot war 解压后的目录 │ ├── WEB-INF/ │ └── ... ├── dist/ # Vue 构建产物 │ ├── index.html │ ├── static/ │ └── ... └── ...

提示:war 包放入 webapps 后,Tomcat 启动时会自动解压。如果修改了 war 包内容,需要先删掉对应的解压目录再重启,否则 Tomcat 可能继续用旧目录。

3. server.xml 里 Context 配置:前端路由与后端接口的路径对齐

3.1 不配 Context 为什么会白屏,配了之后又解决了什么

文档里提到一个关键现象:不添加<Context>配置,登录能成功,但登录之后是空白页。这个问题的根因是 Vue 的 history 路由模式和 Tomcat 的默认上下文路径不匹配。

Vue 项目本地开发时跑在localhost:8080,路由是/login、/dashboard这种形式。打包后如果直接通过ip:端口/dist/index.html访问,Vue Router 在 history 模式下会认为当前路径是/dist/,而它期望的 base 路径是/。结果就是路由匹配不上,页面渲染为空。

在conf/server.xml的<Host>节点下添加<Context>,把path设为/,docBase指向dist的绝对路径,相当于告诉 Tomcat:访问根路径时,直接去dist目录找静态资源。这样 Vue Router 拿到的路径就是/,路由匹配正常。

<!-- conf/server.xml,在 <Host> 节点内添加 --> <Context path="/" docBase="C:/tomcat/webapps/dist" reloadable="true" crossContext="true" />

参数说明:path="/"表示根路径映射;docBase可以是绝对路径,也可以是相对于webapps的路径,比如docBase="dist";reloadable="true"方便开发阶段修改静态文件后自动重载,生产环境建议设为false减少开销;crossContext="true"允许不同上下文之间共享请求,对前后端分离场景下的接口调用有帮助。

3.2 刷新 404 的根因与 WEB-INF/web.xml 兜底方案

配好 Context 之后,首页能正常打开,登录也能跳转。但一按 F5 刷新,404。这个问题的本质是:Vue Router 的 history 模式下,/dashboard这种路径在服务器上并不存在对应的物理文件。首次进入时是 Vue 在前端接管路由,刷新时浏览器直接向 Tomcat 请求/dashboard,Tomcat 找不到这个路径,返回 404。

解决办法是在dist目录下新建WEB-INF文件夹,里面放一个web.xml,把 404 错误重定向到index.html。这样刷新时 Tomcat 返回index.html,Vue Router 重新接管,页面正常渲染。

<!-- dist/WEB-INF/web.xml --> <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <display-name>Router for Tomcat</display-name> <error-page> <error-code>404</error-code> <location>/index.html</location> </error-page> </web-app>

逻辑说明:<error-page>是 Servlet 规范里的错误页面映射。当 Tomcat 返回 404 时,容器会内部转发到/index.html,浏览器地址栏不变,但内容变成了 Vue 的入口页面。metadata-complete="true"表示不使用注解扫描,加快启动速度。

注意:WEB-INF目录必须放在dist文件夹内部,和index.html同级。放到webapps根目录下无效,因为 Context 的docBase指向的是dist。

4. 跨域与第三方 API 调用:Tomcat 层面的处理思路

4.1 跨域问题的来源:前端 dist 和后端 war 不同上下文

前后端分离部署时,前端静态资源由 Tomcat 的dist上下文提供,后端接口由prod-api上下文提供。浏览器发起请求时,如果前端页面地址是ip:端口/,接口地址是ip:端口/prod-api/...,虽然同源策略下端口和协议一致,但路径不同,某些情况下仍会触发跨域限制。更常见的是前端直接调用第三方 API,比如地图服务、短信服务,这些接口的域名和当前页面域名不一致,必然跨域。

文档里提到的「调用第三方 API 如百度地图 API 所存在的跨域问题」,本质是浏览器同源策略拦截了前端直接发起的跨域请求。Tomcat 本身不处理跨域,需要在后端加过滤器或者在前端配置代理。由于没有 Nginx,前端代理这条路走不通,只能在 SpringBoot 侧解决。

4.2 在 SpringBoot 里加全局 CORS 配置的实操

常见做法是在 SpringBoot 里加一个全局 CORS 配置类,允许指定来源的跨域请求。这种方式对第三方 API 调用同样有效,因为请求先到 SpringBoot,再由后端转发出去。

// 全局跨域配置,放在 config 包下 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 拦截所有路径 .allowedOriginPatterns("*") // 允许的来源,生产环境建议写具体域名 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) // 允许携带 Cookie .maxAge(3600); // 预检请求缓存时间,单位秒 } }

参数说明:allowedOriginPatterns在 SpringBoot 2.4 之后替代了allowedOrigins,支持通配符;allowCredentials(true)表示允许跨域请求携带认证信息,此时allowedOriginPatterns不能设为*,需要指定具体域名或使用模式匹配;maxAge(3600)减少 OPTIONS 预检请求的次数。

如果第三方 API 不支持 CORS,前端直接调用会被浏览器拦截。这时候需要在 SpringBoot 里写一个转发接口,由后端去调用第三方 API,再把结果返回给前端。这样浏览器看到的是同源请求,不涉及跨域。

// 后端转发第三方 API 的示例 @RestController @RequestMapping("/proxy") public class ApiProxyController { @GetMapping("/map") public String getMapData(@RequestParam String keyword) { // 实际项目中用 RestTemplate 或 WebClient 调用第三方接口 // 这里只示意转发逻辑 String url = "https://third-party-api.example.com/search?q=" + keyword; // 由后端发起请求,不受浏览器同源策略限制 return restTemplate.getForObject(url, String.class); } }

逻辑说明:浏览器请求/proxy/map,这是同源请求;SpringBoot 收到后,由后端服务器去调用第三方 API,服务器之间的 HTTP 调用没有同源策略限制;拿到结果后再返回给前端。这种方式虽然多了一跳,但能彻底绕开跨域限制,而且可以在后端做缓存、限流和鉴权。

5. 避坑排查:Tomcat 部署 SpringBoot + Vue 的五个高频翻车点

5.1 现象:war 包放入 webapps 后,Tomcat 启动日志报 ClassNotFoundException

原因:pom 里没有排除内嵌 Tomcat,或者启动类没有继承SpringBootServletInitializer。war 包里的WEB-INF/lib同时存在内嵌 Tomcat 的类和 SpringBoot 的类,类加载器冲突。

解决:检查 pom 的 exclusions 配置,确认spring-boot-starter-tomcat的 scope 是provided;检查启动类是否重写了configure方法。改完后重新打包,删掉 webapps 下的旧解压目录再重启。

5.2 现象:登录页正常,登录后白屏,控制台无报错

原因:Vue Router 的 base 路径和 Tomcat 的 Context path 不一致。前端路由期望从/开始,但实际访问路径带了/dist/前缀。

解决:在server.xml的<Host>节点下添加<Context path="/" docBase="dist的绝对路径" />,确保根路径直接映射到 dist 目录。如果项目部署在子路径下,需要同步修改 Vue Router 的base配置。

5.3 现象:刷新页面 404,直接访问路由地址也 404

原因:Vue Router 的 history 模式下,服务器上没有对应的物理文件。Tomcat 默认按路径找文件,找不到就返回 404。

解决:在dist目录下创建WEB-INF/web.xml,配置 404 错误页面重定向到/index.html。注意WEB-INF必须在dist内部,不能放在webapps根目录。

5.4 现象:接口请求返回 403 或 CORS 错误,前端无法获取数据

原因:跨域配置缺失或配置不当。如果前端页面和接口不同源,浏览器会先发 OPTIONS 预检请求,后端没有正确响应就会拦截。

解决:在 SpringBoot 里加全局 CORS 配置,确认allowedMethods包含 OPTIONS,allowedHeaders允许前端实际发送的请求头。如果第三方 API 不支持跨域,改用后端转发方案。

5.5 现象:修改了 dist 里的文件,刷新页面没变化

原因:Tomcat 缓存了静态资源,或者reloadable设为false时不会自动重载。

解决:开发阶段把<Context>的reloadable设为true;生产环境修改后,删掉webapps下的解压目录和work目录下的缓存,再重启 Tomcat。浏览器端强制刷新(Ctrl + F5)清除本地缓存。

6. 验证部署是否真正生效:三个必查项与一个习惯

部署完成后,不要只看首页能不能打开。我一般会按下面三个步骤验证一遍,确保不是「看起来能用」。

第一,直接访问一个前端路由地址,比如ip:端口/dashboard,看是否返回index.html的内容。如果返回 404,说明web.xml的 404 兜底没生效。第二,打开浏览器开发者工具,切到 Network 面板,刷新页面,看静态资源的加载路径是否正确。如果static/js/xxx.js的请求路径带了/dist/前缀,说明 Context 配置的docBase和实际访问路径没对齐。第三,调用一个后端接口,确认返回的是 JSON 而不是 HTML。如果接口返回了index.html的内容,说明 404 兜底把接口请求也重定向了,需要调整web.xml的拦截范围或者把接口路径排除在外。

# 快速验证接口是否正常返回 JSON curl -i http://ip:端口/prod-api/actuator/health # 预期返回类似: # HTTP/1.1 200 # Content-Type: application/json # {"status":"UP"}

如果接口返回的是 HTML,检查web.xml里的<error-page>是否拦截了所有 404。有些项目会把接口路径也重定向到index.html,导致前端拿到 HTML 却以为是 JSON,解析报错。解决办法是在web.xml里只对特定路径做兜底,或者在后端确保接口路径不会返回 404。

还有一个习惯:每次修改server.xml或web.xml之后,先停 Tomcat,删掉webapps下对应的解压目录和work目录下的缓存,再启动。Tomcat 的缓存机制有时候会让人误以为配置没生效,其实是旧文件还在起作用。从那以后我每次部署完都强制走一遍「停服务 → 清缓存 → 启服务 → 验证路由 → 验证接口」的流程,省掉了很多来回折腾的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进