Spring MVC 核心机制深度解析:从一次 HTTP 请求看完整生命周期

技术 · 约 30 分钟

Spring MVC 一次 HTTP 请求的完整生命周期

如果你正在做 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)唯一入口;调度所有组件
HandlerMappingHandlerMappingRequestMappingHandlerMapping把请求 URL 映射到 Handler
HandlerAdapterHandlerAdapterRequestMappingHandlerAdapter调用 Handler 的具体方式
Controller@Controller用户实现业务逻辑
ModelAndViewModelAndView用户返回模型 + 视图名
ViewResolverViewResolverInternalResourceViewResolver视图名 → 实际 View
ViewViewJstlView 等实际渲染(HTML/JSON/PDF)
HandlerInterceptorHandlerInterceptor用户实现拦截器(preHandle/postHandle/after)
HandlerExceptionResolverHandlerExceptionResolverExceptionHandlerExceptionResolver 等异常 → 错误响应

还有四个支撑组件: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
@PathVariableURL 路径变量/user/{id}
@RequestBody请求体(JSON/XML)POST JSON
@RequestHeader请求头User-Agent
@CookieValueCookieJSESSIONID
@RequestAttributerequest 域属性过滤器塞进去的
@SessionAttributesession 域属性登录用户
@ModelAttribute表单自动绑定到对象传统表单
@MatrixVariable矩阵变量(少见)/user;id=42
@RequestPartmultipart 部分文件 + 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 字符串再写回。常见搭配:

模板解析器时代
JSPInternalResourceViewResolver2005–2015 主流
ThymeleafThymeleafViewResolver2015+ 主流
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 或 FilterRegistrationBeanWebMvcConfigurer.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 / JSONJacksonAutoConfiguration提供自己的 ObjectMapper
静态资源WebMvcAutoConfigurationspring.web.resources.*
欢迎页 / favicon同上spring.mvc.favicon.enabled
错误页 / /errorErrorMvcAutoConfiguration实现 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 MVCSpring 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 这一关。

阳熙

Java / 保险业务系统开发 · 无锡

← 返回文章列表

评论

还没有评论,来留第一条吧。

首次评论会做人机校验