如果你正在做 Java Web 开发、用过 Spring Boot、或者准备 Java 岗位面试,Spring MVC 都是绕不开的核心。这篇文章不讲怎么”用” Spring MVC(这个 Spring 官方文档比我写得好),而是讲它内部怎么跑起来——从浏览器敲下回车、到响应回到浏览器,整个链路里到底发生了哪些事、九大核心组件是怎么协作的、面试官最爱追问的几个边界问题在哪里。
一、为什么到 2025 年还要学 Spring MVC
即使 Spring Boot + Spring Cloud 已经是事实标准,Spring MVC 仍是所有 Spring Web 应用的底座:
- Spring WebFlux 的注解式控制器几乎与 Spring MVC 一一对应;
- OW X(NRI 的转型框架)底层切到 Spring 之后,对日大型项目的 Web 层就是 Spring MVC;
- 面试官问”Spring MVC 工作流程”出现的频率,比问”Spring Boot 自动配置原理”还高;
- 调试、生产事故排查、性能优化,最终都要回到这条链路。
理解机制比记住 API 更重要,因为 API 会变、机制稳定。
二、九大核心组件:Spring MVC 的”九块积木”
Spring MVC 的所有行为都可以归结到九个组件的协作。DispatchServlet 在初始化时会从 WebApplicationContext 找这些组件,找不到就用默认实现:
| 组件 | 接口 | 默认实现 | 职责 |
|---|---|---|---|
| DispatcherServlet | (Servlet) | 唯一 | 入口;调度所有组件 |
| HandlerMapping | HandlerMapping | RequestMappingHandlerMapping | 把请求 URL 映射到 Handler |
| HandlerAdapter | HandlerAdapter | RequestMappingHandlerAdapter | 调用 Handler 的具体方式 |
| Controller | @Controller | 用户实现 | 业务逻辑 |
| ModelAndView | ModelAndView | 用户返回 | 模型 + 视图名 |
| ViewResolver | ViewResolver | InternalResourceViewResolver | 视图名 → 实际 View |
| View | View | JstlView 等 | 实际渲染(HTML/JSON/PDF) |
| HandlerInterceptor | HandlerInterceptor | 用户实现 | 拦截器(preHandle/postHandle/after) |
| HandlerExceptionResolver | HandlerExceptionResolver | ExceptionHandlerExceptionResolver 等 | 异常 → 错误响应 |
还有四个支撑组件:LocaleResolver(国际化)、ThemeResolver(主题,已不常用)、MultipartResolver(文件上传)、FlashMapManager(重定向传值)。
记法:DispatcherServlet 是前台,三个”处理”是 Map → Adapter → Controller,三个”响应”是 ModelAndView → ViewResolver → View,两个”拦截”是 Interceptor + ExceptionResolver。
三、一次 HTTP 请求的完整生命周期
这是全文最重要的图(见顶部)。把它走通,Spring MVC 就懂了。
3.1 阶段拆解
浏览器 ──GET /user/42──▶ Tomcat
│
▼
① DispatcherServlet.service() ← Spring MVC 唯一入口
│
▼
② HandlerMapping.getHandler() ← 找到 HandlerExecutionChain
│
▼
③ preHandle(拦截器前置) ← 可以直接拒绝/重定向
│
▼
④ HandlerAdapter.handle() ← 真正调用 Controller 方法
│ ↳ 参数解析、转换、绑定、校验
▼
⑤ Controller 方法执行 ← 业务逻辑
│
▼
⑥ postHandle(拦截器后置) ← Controller 之后、View 之前
│
▼
⑦ ViewResolver → View 渲染 ← (仅返回视图名时)
│ ↳ @ResponseBody 跳过这步
▼
⑧ afterCompletion(拦截器收尾) ← 无论如何都会执行
│
▼
⑨ HttpMessageConverter 写回响应 ← 对象 → 字节流
3.2 关键细节
- 拦截器 vs 过滤器:HandlerInterceptor 是 Spring MVC 层面的,依赖 HandlerExecutionChain;Filter 是 Servlet 规范层面的,由 Servlet 容器管理。Filter 能拦截所有请求(包括静态资源),Interceptor 只能拦截 DispatcherServlet 路由到的请求。
- preHandle 返回 false 会怎样:后面的所有步骤都不执行,但已配置的 afterCompletion 仍会执行(用于清理)。这就是为什么 finally 块的清理逻辑要放在 afterCompletion 而不是 postHandle 里。
- postHandle 注意点:它会在 Controller 执行完、View 渲染前调用,但如果 Controller 里抛了异常,postHandle 不会执行,只有 afterCompletion 会。
- 异常处理在哪一步:异常由 HandlerExceptionResolver 处理,优先于 ViewResolver。这也是
@ControllerAdvice + @ExceptionHandler能截胡异常的原因。
四、参数绑定:从 HTTP 到 Java 对象
参数绑定是 Spring MVC 最让人又爱又恨的部分——用起来方便,但容易踩坑。
4.1 12 种参数来源
| 注解 | 来源 | 示例 |
|---|---|---|
@RequestParam | 查询参数 / 表单参数 | ?id=42 |
@PathVariable | URL 路径变量 | /user/{id} |
@RequestBody | 请求体(JSON/XML) | POST JSON |
@RequestHeader | 请求头 | User-Agent |
@CookieValue | Cookie | JSESSIONID |
@RequestAttribute | request 域属性 | 过滤器塞进去的 |
@SessionAttribute | session 域属性 | 登录用户 |
@ModelAttribute | 表单自动绑定到对象 | 传统表单 |
@MatrixVariable | 矩阵变量(少见) | /user;id=42 |
@RequestPart | multipart 部分 | 文件 + JSON |
HttpServletRequest/Response/... | 直接拿到 Servlet API | 特殊场景 |
| 简单类型(无注解) | 按参数名匹配 | @RequestParam 隐式版 |
4.2 容易踩的坑
// ❌ POST 没指定 consumes,默认会接 application/json
// 但浏览器表单提交是 application/x-www-form-urlencoded
@PostMapping("/save")
public Result save(@RequestBody UserDTO dto) // 表单提交到这里就 415 了
// ✅ 显式声明 consumes
@PostMapping(value = "/save", consumes = MediaType.APPLICATION_JSON_VALUE)
public Result save(@RequestBody UserDTO dto)
// ❌ 简单类型不加注解,required=true 是默认值
@GetMapping("/get")
public User get(@RequestParam Long id) // 不传 id 会 400
// ✅ 区分可空与必填
public User get(@RequestParam(required = false) Long id)
// ❌ @PathVariable 默认必填
@GetMapping("/user/{id}")
public User get(@PathVariable Long id) // /user 会 404
// ✅ 用 optional 路径
@GetMapping({"/user/{id}", "/user"})
public User get(@PathVariable(required = false) Long id) {
return id == null ? currentUser() : userService.get(id);
}
4.3 数据绑定的全过程
HTTP 字符串 "2026-09-15"
│
▼
① ConversionService 转换(String → LocalDate)
│
▼
② Validator 校验(@Valid 注解触发)
│
▼
③ BindingResult 收集错误(不抛异常,由代码决定后续)
│
▼
④ 注入到 @ModelAttribute / @RequestParam 参数
校验失败时不会自动返回 400,而是把错误塞进 BindingResult——除非方法签名里没有 BindingResult 参数,Spring MVC 才会抛 BindException(Spring 5+ 行为)。
五、HttpMessageConverter:请求体/响应体的翻译官
Spring MVC 不会自己解析 JSON。它靠一串 HttpMessageConverter:
请求进来: 字节流 ──▶ ByteArrayHttpMessageConverter
──▶ StringHttpMessageConverter
──▶ FormHttpMessageConverter
──▶ MappingJackson2HttpMessageConverter ← JSON
──▶ MappingJackson2XmlHttpMessageConverter
──▶ Jaxb2RootElementHttpMessageConverter
(按 Content-Type 选第一个支持的)
5.1 自定义 Converter
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 全局日期格式:统一 ISO 8601
MappingJackson2HttpMessageConverter jackson =
new MappingJackson2HttpMessageConverter();
jackson.setObjectMapper(new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS));
converters.add(0, jackson); // 0 = 最高优先级
}
}
5.2 容易踩的坑
// ❌ 直接返回 LocalDateTime,Jackson 默认转成数组 [2026,9,15,...]
public LocalDateTime getDate() { return LocalDateTime.now(); }
// ✅ 加 Jackson 配置或字段注解
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
六、视图解析(前后端分离时代基本可以略过)
传统 Spring MVC 流程会走 ViewResolver → View,把 Model 渲染成 HTML 字符串再写回。常见搭配:
| 模板 | 解析器 | 时代 |
|---|---|---|
| JSP | InternalResourceViewResolver | 2005–2015 主流 |
| Thymeleaf | ThymeleafViewResolver | 2015+ 主流 |
| Freemarker / Velocity | 对应 ViewResolver | 已边缘化 |
| JSON / XML | 不走视图(@ResponseBody 短路) | 2018+ 主流 |
到 2025 年纯后端 API 项目基本不需要 ViewResolver——所有 Controller 都是 @RestController,返回值直接走 HttpMessageConverter 写 JSON。理解这套机制是历史包袱,但前后端分离不彻底的项目还是会用到(比如对日项目里常见的 JSP + AJAX 混合)。
七、拦截器 vs 过滤器:面试必问的边界
这是最容易混淆的一对概念:
| 维度 | Filter(过滤器) | HandlerInterceptor(拦截器) |
|---|---|---|
| 规范 | Servlet 规范 | Spring MVC 规范 |
| 实现 | 实现 jakarta.servlet.Filter | 实现 HandlerInterceptor |
| 注册 | @WebFilter 或 FilterRegistrationBean | WebMvcConfigurer.addInterceptors() |
| 拦截范围 | 所有请求(含静态资源) | 仅 DispatcherServlet 路由到的请求 |
| 能否拿 Handler 信息 | 不能 | 能(preHandle 第二参数) |
| 异常处理 | 抛 ServletException | 由 HandlerExceptionResolver 处理 |
| AOP 切面能用 | 不能(Spring AOP 不管它) | 能 |
典型用法
// Filter:处理跨域、字符编码、请求日志(对所有请求生效)
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
chain.doFilter(req, res);
}
}
// Interceptor:登录校验、权限检查(只对 Controller 生效)
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
if (req.getSession().getAttribute("user") == null) {
res.sendRedirect("/login");
return false; // 拦截
}
return true;
}
}
记法:Filter 是 Servlet 容器的事,Interceptor 是 Spring MVC 的事。Interceptor 拿不到的东西用 Filter 补(比如请求体的多次读取、Servlet 级别的异常)。
八、异常处理:四个层级
Spring MVC 的异常处理分四层,从内到外:
① try-catch(方法内) ← 最具体,可读性最好
② @ExceptionHandler(同 Controller) ← 只管本 Controller
③ @ControllerAdvice + @ExceptionHandler ← 全局,可继承 BaseController 共享
④ HandlerExceptionResolver 接口 ← 框架级,最不常用
8.1 推荐写法
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
return Result.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(400, msg);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleUnknown(Exception e) {
log.error("系统异常", e); // 必须记录日志
return Result.fail(500, "系统繁忙,请稍后再试");
}
}
8.2 关键细节
- 优先级:方法内 try-catch > Controller 内 @ExceptionHandler > @ControllerAdvice > 默认处理;
- 要返回 ResponseEntity 还是自定义 Result:保持全站统一(不要 Controller A 返回 Result、Controller B 返回 ResponseEntity);
- 记得打日志:很多项目全局异常处理器只返回用户提示,不打日志,线上故障复盘时根本查不到;
- 不要在 ControllerAdvice 里做业务:它只负责”统一翻译”,业务回滚、补偿、重试应该由业务代码或 AOP 负责。
九、异步请求:@Async 与 Callable/DeferredResult
Spring MVC 从 3.2 开始支持异步请求,目的是释放 Servlet 容器线程——把耗时操作(远程调用、长任务)放到另一个线程池,Servlet 线程立刻返回,让容器能接更多请求。
9.1 三种方式
// 方式 1:返回 Callable(Spring 自动用 TaskExecutor 执行)
@GetMapping("/async1")
public Callable<User> async1() {
return () -> {
Thread.sleep(1000); // 模拟耗时
return userService.get(42);
};
}
// 方式 2:返回 DeferredResult(更灵活,自己管理异步线程)
@GetMapping("/async2")
public DeferredResult<User> async2() {
DeferredResult<User> dr = new DeferredResult<>(5000L); // 5s 超时
CompletableFuture.supplyAsync(() -> userService.get(42), taskExecutor)
.whenComplete((r, ex) -> {
if (ex != null) dr.setErrorResult(ex);
else dr.setResult(r);
});
return dr;
}
// 方式 3:@Async(Spring AOP 实现,需要开启 @EnableAsync)
@Async
public CompletableFuture<User> async() { ... }
9.2 异步请求的坑
| 坑 | 后果 |
|---|---|
| 拦截器 / Filter 没意识到异步 | 异常丢失;MDC/TraceId 上下文跨线程丢失 |
request.getParameter() 在异步线程调用 | 取不到值(请求已被回收) |
| 超时设置不当 | 大量超时线程堆积 |
| 异常处理在异步线程抛出 | @ControllerAdvice 抓不到(除非用 CompletableFuture.whenComplete) |
| Servlet 3.0 之前 | 不支持 asyncSupported,要 web.xml 配置 |
十、Spring MVC 与 Spring Boot 的关系
很多人把 Spring MVC 和 Spring Boot 当并列概念,其实是包含关系:
Spring Framework
├── Core Container(IoC / AOP)
├── Data Access(JdbcTemplate / ORM 集成)
├── Web(MVC / WebFlux) ← Spring MVC 在这
└── ...
Spring Boot
└── 自动配置 Spring MVC:
- DispatcherServlet 不用手配
- 默认带了 Jackson、Validation、Tomcat
- 静态资源映射、欢迎页、错误页都开箱即用
- 但底层还是 Spring MVC 那套组件
10.1 Spring Boot 自动配置了什么
| 默认行为 | 配置类 | 想改怎么办 |
|---|---|---|
| DispatcherServlet 注册 | DispatcherServletAutoConfiguration | 改 server.servlet.* |
| Jackson / JSON | JacksonAutoConfiguration | 提供自己的 ObjectMapper |
| 静态资源 | WebMvcAutoConfiguration | spring.web.resources.* |
| 欢迎页 / favicon | 同上 | spring.mvc.favicon.enabled |
错误页 / /error | ErrorMvcAutoConfiguration | 实现 ErrorController |
10.2 Spring Boot 3.x 的关键变化
- Java 17+ 强制(Spring 6+);
javax.servlet.*改名jakarta.servlet.*(Jakarta EE 9);- 自动配置用
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(替代了spring.factories); - GraalVM 原生镜像支持(Spring Native);
- 默认日志改为 Logback(无变化,但 LoggingSystem 接口统一了)。
十一、对日项目中的 Spring MVC
承接上一篇 NRI 框架文章(ObjectWorks 全景)的内容,OW X 转型之后,对日大型系统的 Web 层就是 Spring MVC。但具体落地时会有这些常见模式:
| 模式 | 场景 | 实现方式 |
|---|---|---|
| 既存系统 JSP 改造 | ow+ JSP → Spring MVC | 保留 JSP 视图,加 @Controller |
| REST API 新建 | 前后端分离项目 | @RestController + Swagger |
| 批处理 HTTP 接口 | 给外部系统调用的批跑接口 | @RestController + 异步 + 幂等 |
| 与 SAP 集成 | ERP 联动 | Spring MVC + Feign/SAP JCo |
| 报表接口 | 大量数据导出 | StreamingResponseBody(避免 OOM) |
OW X 项目的 Spring MVC 通常配合自家规约(异常码、MessageId、TraceId、MDC 日志上下文),理解 Spring MVC 机制是看懂 OW X 项目代码的前提。
十二、面试高频问题与简明回答
Q1:Spring MVC 工作流程?
照着本文第三节的图答。画图比背文字更稳。
Q2:@Controller 和 @RestController 区别?
@RestController = @Controller + @ResponseBody(类级别)。@ResponseBody 让返回值走 HttpMessageConverter 写 JSON,跳过 ViewResolver。
Q3:Spring MVC 的单例 Controller 怎么保证线程安全?
- Controller 默认单例,不要在 Controller 字段里放可变状态;
- 通过方法参数传对象(Spring 会每次请求 new 一个 BindingResult);
- 需要”实例变量”时用
@Scope("prototype"),但很少这么做。
Q4:拦截器和过滤器区别?
见第七节。Filter 是 Servlet 规范、Interceptor 是 Spring MVC。
Q5:@RequestMapping 和 @GetMapping 区别?
@GetMapping = @RequestMapping(method = RequestMethod.GET),更精确、性能略好(HandlerMapping 索引更细)。
Q6:Spring MVC 怎么统一异常处理?
@RestControllerAdvice + @ExceptionHandler,注意日志一定要打。
Q7:Spring MVC 怎么统一响应格式?
两种思路:
@RestControllerAdvice+ 实现ResponseBodyAdvice<Object>(拦截所有 Controller 返回值);- AOP 切
@RestController(侵入性更低,但性能略差)。
Q8:为什么 Spring MVC 设计成 DispatcherServlet 单入口?
- 单一职责:一个 Servlet 处理所有请求,便于统一处理(异常、拦截、日志);
- 可扩展:所有组件都是接口,可以替换;
- 解耦:Controller 不依赖 Servlet API,便于测试。
Q9:Spring MVC 和 Spring WebFlux 区别?
| 维度 | Spring MVC | Spring WebFlux |
|---|---|---|
| 编程模型 | 同步、阻塞 | 异步、非阻塞 |
| 线程模型 | 一个请求一个线程 | 事件循环 |
| 适用场景 | 传统业务 | 高并发 I/O 密集 |
| 学习成本 | 低 | 高 |
到 2025 年 Spring MVC 仍是大多数项目的首选,WebFlux 主要用于网关、流式接口等特殊场景。
十三、一页清单(保存这张就能面 Spring MVC)
入口:DispatcherServlet(单 Servlet 总调度)
三处理:HandlerMapping → HandlerAdapter → @Controller
三响应:ModelAndView → ViewResolver → View
两拦截:HandlerInterceptor(pre/post/after)+ HandlerExceptionResolver
四支撑:Locale / Theme / Multipart / FlashMap
请求生命周期:DispatcherServlet → HandlerMapping → preHandle
→ HandlerAdapter → Controller → postHandle
→ ViewResolver → View → afterCompletion
参数注解:@RequestParam @PathVariable @RequestBody @RequestHeader
@CookieValue @RequestAttribute @SessionAttribute @ModelAttribute
数据转换:HttpMessageConverter(Jackson / String / ByteArray / Form)
异常处理:try-catch → @ExceptionHandler → @ControllerAdvice → 框架默认
异步请求:Callable / DeferredResult / @Async(注意上下文丢失)
拦截器 vs 过滤器:Interceptor 是 Spring MVC 层,Filter 是 Servlet 层
Spring Boot = Spring MVC + 自动配置 + 嵌入式容器
后续可以挖的方向
- Spring MVC 在 GraalVM 原生镜像下的反射处理(Spring 6 的优化点);
- 与 Spring Security 的集成(FilterChainProxy 在 Filter 链中的位置);
- Spring MVC 性能调优(连接池、线程池、StreamingResponseBody);
- OW X 项目的 Spring MVC 规约与定制点(接着上一篇文章展开)。
如果你正在准备 Java 面试,建议把本文的第十三节当 checklist——每一项都能展开讲 5 分钟,就算过 Spring MVC 这一关。
评论
还没有评论,来留第一条吧。