一、项目背景
技术栈
- 前端:Vue 3 + Vite + Vue Router(History 模式)
- 后端:Spring Boot 3 + MyBatis-Plus
- 部署方式 :Vue 打包后的静态资源放入 Spring Boot 的
src/main/resources/static/目录,由 Spring Boot 统一提供服务
问题现象
打包部署后,访问 localhost:8080 出现以下错误:
- 500 Internal Server Error(Whitelabel Error Page)
- 404 Not Found(跳转子页面或刷新时)
二、错误案例剖析
案例 1:500 错误 - Whitelabel Error Page
-
错误日志
There was an unexpected error (type=Internal Server Error, status=500). java.lang.StackOverflowError: null at org.springframework.web.servlet.DispatcherServlet.doDispatch(...) -
问题代码(错误做法)
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 错误:会导致循环转发 registry.addViewController("/*") .setViewName("forward:/index.html"); } } -
原因分析
- 用户访问
http://localhost:8080/ ViewControllerRegistry匹配到/*,转发到/index.html- Spring Boot 的静态资源处理器 也开始处理
/index.html - DispatcherServlet 和静态资源处理器产生冲突
- 请求在两者之间循环转发 ,最终栈溢出 → 500 错误
- 用户访问
-
核心矛盾
WebMvcConfigurer是在 DispatcherServlet 层面拦截请求- 静态资源处理器也是在 DispatcherServlet 的过滤链中
- 两者处理同一个请求时产生死循环
案例 2:404 错误 - 跳转子页面或刷新时
-
场景重现
- 首页正常显示:
http://localhost:8080/ - 在 Vue 内点击按钮跳转到
/window(前端路由,无请求) - 直接输入 URL :
http://localhost:8080/window404 - 刷新子页面 :F5 刷新
/window404
- 首页正常显示:
-
原因分析
用户访问:http://localhost:8080/window ↓ 浏览器发送请求:GET /window ↓ Spring Boot 收到请求 ↓ 查找 Controller:没有 @RequestMapping("/window") ✗ ↓ 查找静态资源:static/ 目录下没有 window 文件 ✗ ↓ 返回 404 Not Found -
核心问题
- Vue Router 使用 History 模式,URL 看起来像真实路径
- 服务器不知道
/window是前端路由标识 - 服务器尝试查找真实资源,找不到就返回 404
三、正确解决方案:SpaForwardFilter
完整代码
package com.example.liverank.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class SpaForwardFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
// 排除 API 请求和静态资源
if (path.startsWith("/api/") ||
path.contains(".") ||
path.equals("/")) {
chain.doFilter(request, response);
return;
}
// 其他所有请求转发到 index.html(支持 Vue History 模式)
request.getRequestDispatcher("/index.html").forward(request, response);
}
}
代码逐行解析
@Component注解- 将 Filter 注册为 Spring Bean
- Spring Boot 自动将其添加到 Servlet 过滤链中
implements Filter- 实现 Servlet 规范的 Filter 接口
- 重写
doFilter()方法处理请求
- 请求路径判断逻辑
-
// 条件 1:放行 API 请求if (path.startsWith("/api/")) { chain.doFilter(request, response); return; }作用 :所有
/api/*的请求(如/api/staff/list)直接放行,交给 Spring MVC 的 Controller 处理。 -
// 条件 2:放行静态资源if (path.contains(".")) { chain.doFilter(request, response); return; }作用 :包含
.的路径认为是静态资源(如/assets/index.js、/favicon.svg),放行给 Spring Boot 的静态资源处理器。 -
// 条件 3:放行根路径if (path.equals("/")) { chain.doFilter(request, response); return; }作用 :用户首次访问
http://localhost:8080/时,直接返回 index.html。 -
// 核心逻辑:转发所有 Vue 路由请求request.getRequestDispatcher("/index.html").forward(request, response);作用 :其他所有路径(如
/window、/admin)统一转发到index.html,让 Vue Router 处理。
-
四、为什么 Filter 能解决问题?
请求处理链的优先级
浏览器请求
↓
┌─────────────────────────────────────┐
│ Filter(最早拦截) │
│ ├─ API 请求 → 放行 │
│ ├─ 静态资源 → 放行 │
│ └─ Vue 路由 → 转发到 index.html │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ DispatcherServlet(Spring MVC) │
│ ├─ 查找 @RestController │
│ └─ 查找静态资源处理器 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Controller / 静态资源返回 │
└─────────────────────────────────────┘
Filter vs WebMvcConfigurer 对比
| 特性 | Filter | WebMvcConfigurer |
|---|---|---|
| 执行时机 | Servlet 容器层面(最早) | DispatcherServlet 之后 |
| 与静态资源冲突 | 不会冲突 | 会冲突(500 错误) |
| 适用范围 | 所有请求 | 仅 Spring MVC 管理的请求 |
| 推荐场景 | SPA 路由转发、跨域、认证 | 视图映射、消息转换器 |
五、完整请求流程图
场景:用户访问 http://localhost:8080/window
1. 浏览器发送 GET /window
↓
2. SpaForwardFilter 拦截
path = "/window"
↓
3. 判断逻辑
├─ 是 /api/* 吗? → 否 ✗
├─ 包含 "." 吗? → 否 ✗
─ 等于 "/" 吗? → 否 ✗
↓
4. 执行 forward("/index.html")
↓
5. 服务器返回 index.html
(包含 Vue 应用的 JS/CSS)
↓
6. 浏览器加载 Vue 应用
↓
7. Vue Router 读取 URL:/window
↓
8. 匹配路由规则,加载 WindowPage 组件
↓
9. 页面正常显示
六、面试回答模板
面试官问:"Vue 部署到 Spring Boot 遇到 404/500 怎么解决?"
标准回答:
"这个问题是因为 Vue Router 使用了 History 模式导致的。
404 的原因 :当用户直接访问子页面 URL(如
/window)或刷新页面时,浏览器会向服务器发送请求。Spring Boot 收到请求后,尝试查找对应的 Controller 或静态资源,找不到就返回 404。实际上/window只是 Vue 的前端路由标识,不是真实路径。500 的原因 :我之前尝试用
WebMvcConfigurer的addViewController来转发请求,但这会和 Spring Boot 的静态资源处理器产生冲突,导致循环转发和栈溢出,最终报 500 错误。解决方案 :我使用了一个
Filter,在请求到达 DispatcherServlet 之前就进行拦截。对于 API 请求(/api/*)和静态资源(包含.的路径)直接放行,其他所有路径统一转发到index.html。这样 Vue 应用就能正常加载,由 Vue Router 来解析 URL 并显示对应的组件。为什么用 Filter:Filter 的执行时机在 Servlet 容器层面,比 DispatcherServlet 更早,避免了与静态资源处理器的冲突。这是 Spring Boot 官方推荐的 SPA 部署方案。"
七、扩展知识
forward() vs sendRedirect()
| 方法 | 特点 | URL 变化 | 请求次数 | 适用场景 |
|---|---|---|---|---|
| forward() | 服务器内部转发 | 不变 | 1 次 | SPA 路由转发 |
| sendRedirect() | 客户端重定向 | 改变 | 2 次 | 登录跳转、外部链接 |
我们使用 forward() 是为了保持 URL 不变,让 Vue Router 能正确解析。
生产环境最佳实践
实际生产环境通常使用 Nginx 反向代理:
server {
listen 80;
# 静态资源
location / {
root /var/www/live-rank;
try_files $uri $uri/ /index.html;
}
# API 转发
location /api/ {
proxy_pass http://localhost:8080;
}
}
优势:
- 静态资源由 Nginx 直接处理,性能更高
- 减轻 Spring Boot 服务器的压力
- 更容易做负载均衡和 CDN 加速
八、总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 500 错误 | WebMvcConfigurer 与静态资源处理器冲突 | 改用 Filter |
| 404 错误 | 服务器不认识 Vue 路由,以为是真实路径 | Filter 转发到 index.html |
| 刷新子页面 404 | 浏览器发送请求,服务器找不到资源 | Filter 统一处理 |
核心原理一句话:
"Filter 在请求处理链的最前端,通过判断请求类型,将 Vue 路由请求转发到 index.html,避免与 Spring Boot 静态资源处理器冲突,完美支持 SPA History 模式。"
九、学习建议
- 理解 HTTP 请求的生命周期:Filter → Interceptor → Controller
- 掌握 forward 和 redirect 的区别:面试常考点
- 了解前端路由的两种模式:Hash vs History
- 动手实验:打开浏览器 Network 面板,观察不同操作的网络请求