Spring MVC 请求处理流程与核心源码解析

摘要

在 Spring Boot 项目中,我们只需要在方法上添加 @GetMapping 或 @PostMapping,就可以让一个 Java 方法接收 HTTP 请求。但从请求到达服务器,到 Controller 方法真正执行,中间经历了 Servlet 容器、DispatcherServlet、HandlerMapping、HandlerAdapter、参数解析器、消息转换器和异常解析器等多个组件。

本文以一次 Spring MVC 请求为主线,先建立整体认知,再沿着 DispatcherServlet 的核心处理流程分析 Spring MVC 如何完成路由匹配、参数绑定、Controller 调用、返回值序列化和异常处理。文章不会陷入所有源码细节,而是抓住最值得调试和排查的主干。

读完本文后,你应该能够:

  • 说清楚 Spring MVC 请求处理的主要阶段;
  • 理解 DispatcherServlet 和各类 MVC 组件的职责;
  • 区分 Filter、Interceptor、Controller 和 AOP 的执行位置;
  • 通过断点和日志定位路由、参数、序列化和异常问题;
  • 进一步阅读 Spring MVC 源码并建立自己的调用链地图。

一、背景与问题

1. 一个注解背后发生了什么

下面是一个看起来非常简单的接口:

java 复制代码
@GetMapping("/users/{id}")
public UserResponse findById(
        @PathVariable Long id,
        @RequestParam(defaultValue = "false") boolean detail) {
    return userService.findById(id, detail);
}

客户端发送:

http 复制代码
GET /api/users/42?detail=true HTTP/1.1
Host: localhost:8080
Accept: application/json

Spring MVC 需要完成很多工作:

  1. 找到可以处理这个请求的 Controller 方法;
  2. 判断请求方法和路径是否匹配;
  3. 从路径中解析 id;
  4. 从查询参数中解析 detail;
  5. 将字符串转换成 Long 和 boolean;
  6. 调用 Controller 方法;
  7. 将返回的 Java 对象转换为 JSON;
  8. 设置状态码和响应头;
  9. 如果中间出现异常,将异常转换成响应。

如果只把这些能力归因于"Spring 自动完成了",遇到 404、400、415、500 或响应格式不对时,就很难知道应该从哪里开始排查。

2. 为什么需要理解源码主线

日常开发不需要每天阅读框架源码,但以下问题通常需要理解 Spring MVC 的处理过程:

  • Controller 明明存在,却匹配不到路由;
  • 同一路径有多个方法,启动或调用时报冲突;
  • @RequestBody 无法解析;
  • 自定义参数解析器没有生效;
  • Controller 抛出的异常没有被统一处理;
  • 接口进入了拦截器,却没有进入 Controller;
  • 返回值没有被序列化成预期格式;
  • 异步请求和普通同步请求行为不同。

源码阅读的目标不是记住每一行实现,而是知道某个问题属于哪一层,以及下一步应该观察哪个组件。

3. Spring MVC 的核心问题

Spring MVC 主要解决的是"如何把 HTTP 请求映射到 Java 方法,并把 Java 结果转换回 HTTP 响应"。

可以拆成四个问题:

text 复制代码
请求是谁处理的?
  -> DispatcherServlet

应该调用哪个方法?
  -> HandlerMapping + HandlerAdapter

方法参数如何得到?
  -> HandlerMethodArgumentResolver

返回值和异常如何转换?
  -> ReturnValueHandler + HandlerExceptionResolver

二、核心概念

1. DispatcherServlet:前端控制器

DispatcherServlet 是 Spring MVC 的核心入口。它本身不承载具体业务,而是作为统一的前端控制器:

text 复制代码
所有符合条件的请求
      ↓
DispatcherServlet
      ↓
找到处理器
      ↓
执行处理器
      ↓
处理返回值或异常
      ↓
生成 HTTP 响应

在 Spring Boot Web 应用中,DispatcherServlet 通常由自动配置注册到内嵌 Servlet 容器中。Tomcat 负责接收网络请求,DispatcherServlet 负责在 Spring MVC 中组织处理流程。

2. Handler:请求处理器

Handler 是 Spring MVC 对"请求处理目标"的抽象。它可以是:

  • 一个带有 @RequestMapping 的 Controller 方法;
  • 一个实现特定接口的处理器;
  • 其他可被 HandlerAdapter 执行的对象。

对于注解式 Controller,处理器通常会被封装成 HandlerMethod。HandlerMethod 不仅包含目标方法,还包含 Controller 实例、方法参数和注解元数据。

3. HandlerMapping:寻找处理器

HandlerMapping 负责根据请求信息查找处理器。请求信息可能包括:

  • HTTP 方法;
  • URI 路径;
  • 请求头;
  • 参数条件;
  • 内容类型;
  • 自定义匹配条件。

常见的注解式请求映射会被 Spring 预先解析并注册。应用启动时,Spring 会扫描 Controller 上的映射注解,构建路径到处理器的映射关系。

例如:

java 复制代码
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public OrderResponse get(@PathVariable Long id) {
        return orderService.get(id);
    }
}

最终形成的逻辑映射可以理解为:

text 复制代码
GET /api/orders/{id}
  -> OrderController.get(Long)

4. HandlerAdapter:执行处理器

HandlerMapping 找到 Handler 后,DispatcherServlet 不一定直接调用它,而是交给 HandlerAdapter。

这样设计的好处是:不同类型的 Handler 可以通过不同的适配器执行,DispatcherServlet 不需要了解每种处理器的具体调用方式。

对于带注解的 Controller 方法,常见的是 RequestMappingHandlerAdapter。它负责:

  • 准备方法调用环境;
  • 解析方法参数;
  • 执行 Controller 方法;
  • 处理方法返回值;
  • 协调数据绑定、校验和消息转换。

5. HandlerMethodArgumentResolver:解析方法参数

Controller 方法参数的来源可能不同:

java 复制代码
public UserResponse find(
        @PathVariable Long id,
        @RequestParam String keyword,
        @RequestHeader("X-Trace-Id") String traceId,
        @RequestBody SearchRequest request,
        Principal principal) {
    // ...
}

Spring MVC 会根据参数类型和注解选择不同的参数解析器:

参数类型 常见解析方式
@PathVariable 从 URI 模板变量中读取
@RequestParam 从查询参数或表单参数中读取
@RequestHeader 从请求头中读取
@CookieValue 从 Cookie 中读取
@RequestBody 使用 HttpMessageConverter 读取请求体
@ModelAttribute 创建对象并绑定请求参数
Principal 从安全上下文中获取当前身份
HttpServletRequest 直接注入 Servlet 请求对象

如果没有合适的解析器,或者转换失败,请求通常会在 Controller 方法执行之前结束,并返回 400 等错误。

6. HttpMessageConverter:对象与 HTTP 内容转换

@RequestBody 和 @ResponseBody 依赖 HttpMessageConverter 完成数据转换。

请求方向:

text 复制代码
JSON 请求体
  -> HttpMessageConverter
  -> Java 请求对象

响应方向:

text 复制代码
Java 返回对象
  -> HttpMessageConverter
  -> JSON 响应体

转换器的选择通常会受到以下因素影响:

  • 请求或响应的 Content-Type;
  • 客户端的 Accept;
  • Java 参数或返回值类型;
  • 当前注册的转换器顺序;
  • 是否存在合适的媒体类型。

如果请求体是 JSON,却没有正确设置 Content-Type,或者项目缺少 JSON 转换器,就可能出现 415 或请求体解析失败。

7. HandlerMethodReturnValueHandler:处理返回值

Controller 方法可以返回多种类型:

java 复制代码
@ResponseBody
public UserResponse get() {
    return user;
}
java 复制代码
public ResponseEntity<UserResponse> get() {
    return ResponseEntity.ok(user);
}
java 复制代码
public void download(HttpServletResponse response) {
    // 直接写入响应
}

Spring MVC 会根据返回值类型和注解选择处理器:

  • @ResponseBody:将返回值写入响应体;
  • ResponseEntity:同时控制状态码、响应头和响应体;
  • String:在传统视图模式下可能代表视图名称;
  • void:可能由方法直接处理响应;
  • StreamingResponseBody:支持流式写回;
  • DeferredResult 或 Callable:支持异步请求处理。

8. HandlerExceptionResolver:处理异常

请求处理过程中可能出现:

  • 参数绑定异常;
  • 参数校验异常;
  • 业务异常;
  • 权限异常;
  • 数据库异常;
  • 未处理的运行时异常。

HandlerExceptionResolver 负责尝试将异常转换为 ModelAndView、响应体或状态码。@ExceptionHandler、@ControllerAdvice 和 @RestControllerAdvice 都建立在 Spring MVC 的异常解析机制之上。

9. Filter、Interceptor 和 AOP 的位置

它们都能实现横切逻辑,但执行层次不同:

text 复制代码
Servlet 容器
  -> Filter
      -> DispatcherServlet
          -> Interceptor.preHandle
              -> Controller
          -> Interceptor.postHandle
          -> Interceptor.afterCompletion
      -> Filter 返回
  -> AOP 代理方法边界

AOP 的实际位置取决于被代理的 Bean 方法。Controller 方法也可能被 AOP 代理,但 Filter 和 Interceptor 的生命周期与 Servlet、Spring MVC 更直接相关。

三、工作原理

1. DispatcherServlet 的主流程

一次典型请求可以抽象成:

源码阅读时,可以将 DispatcherServlet 的请求入口看作一条主干,然后再进入各个扩展点,而不是一开始就从项目启动源码一路追踪。

2. doDispatch:请求分发的核心方法

在 DispatcherServlet 的核心逻辑中,可以用伪代码表示 doDispatch 的职责:

java 复制代码
protected void doDispatch(HttpServletRequest request,
                          HttpServletResponse response) {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;

    try {
        processedRequest = checkMultipart(request);

        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }

        HandlerAdapter adapter = getHandlerAdapter(
                mappedHandler.getHandler()
        );

        ModelAndView modelAndView = adapter.handle(
                processedRequest,
                response,
                mappedHandler.getHandler()
        );

        processDispatchResult(
                processedRequest,
                response,
                mappedHandler,
                modelAndView
        );
    } catch (Exception exception) {
        processDispatchResult(
                processedRequest,
                response,
                mappedHandler,
                null,
                exception
        );
    }
}

实际源码还会处理异步请求、文件上传、请求属性恢复和结果清理等情况,但主线可以归纳为:

  1. 准备请求;
  2. 找 Handler;
  3. 找 HandlerAdapter;
  4. 调用 Handler;
  5. 处理返回值;
  6. 处理异常;
  7. 完成响应。

3. getHandler:找到 HandlerExecutionChain

getHandler 不只返回一个 Controller 方法,还可能返回 HandlerExecutionChain:

text 复制代码
HandlerExecutionChain
  = Handler
  + Interceptor 列表

因此,Spring MVC 找到路由后,还会把匹配到的拦截器一起组织起来。请求进入 Controller 前,拦截器可以执行认证、权限、限流或上下文初始化。

如果请求没有匹配到 Handler,应用可能返回 404;如果匹配到了 Handler 但 HTTP 方法不正确,可能返回 405。

4. HandlerAdapter 如何调用 Controller

对于注解式 Controller,RequestMappingHandlerAdapter 会调用一个可执行的方法模型。简化来看:

text 复制代码
Controller 方法
  -> 创建 WebDataBinder
  -> 遍历方法参数
  -> 找到支持当前参数的 Resolver
  -> 解析并转换参数
  -> 执行 Controller 方法
  -> 找到支持返回值的 Handler
  -> 处理返回值

参数解析顺序通常与方法参数顺序相关。解析器会读取请求中的路径变量、查询参数、请求体、请求头或上下文对象,并完成类型转换和校验。

5. 参数解析和数据绑定

以 @RequestBody 为例:

java 复制代码
@PostMapping("/users")
public UserResponse create(
        @Valid @RequestBody CreateUserRequest request) {
    return userService.create(request);
}

大致过程是:

  1. 识别参数上存在 @RequestBody;
  2. 根据请求 Content-Type 选择消息转换器;
  3. 从请求体读取字节;
  4. 反序列化为 CreateUserRequest;
  5. 如果存在 @Valid,执行 Bean Validation;
  6. 校验失败则抛出参数校验异常;
  7. 校验成功后将对象传给 Controller。

这里的每一步都可能失败。比如请求体为空、JSON 格式错误、字段类型不匹配和校验不通过,最终可能表现为不同的异常。

6. 返回值处理和响应写回

以返回 UserResponse 为例:

text 复制代码
Controller 返回 UserResponse
  -> 判断是否使用响应体
  -> 选择 JSON 消息转换器
  -> 序列化对象
  -> 设置 Content-Type
  -> 写入 HttpServletResponse
  -> 容器将字节发送给客户端

如果返回 ResponseEntity,状态码和响应头会先从 ResponseEntity 中读取,再处理 body。这样可以让业务接口明确返回 201、204、404 等状态。

7. 异常处理主线

异常处理可以简化为:

text 复制代码
Controller 或参数解析阶段抛出异常
  -> DispatcherServlet 捕获
  -> 遍历异常解析器
  -> 查找 @ExceptionHandler 或默认处理方式
  -> 生成状态码和响应体
  -> 写回客户端

使用统一异常处理时,建议将异常分为:

  • 参数异常:返回 400;
  • 未认证:返回 401;
  • 无权限:返回 403;
  • 资源不存在:返回 404;
  • 业务冲突:返回 409;
  • 服务内部异常:返回 500。

错误响应应该稳定、简洁,不应泄露堆栈、SQL 和内部路径。

四、实战示例

下面实现一个带参数校验、拦截器、统一异常处理和请求追踪的 Spring MVC 接口。

1. 定义请求对象

java 复制代码
package com.example.demo.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record CreateUserRequest(
        @NotBlank(message = "用户名不能为空")
        @Size(max = 50, message = "用户名不能超过 50 个字符")
        String name,

        @NotBlank(message = "邮箱不能为空")
        @Email(message = "邮箱格式不正确")
        String email
) {
}

请求对象负责表达接口输入约束。不要把数据库 Entity 直接作为接口入参,因为数据库字段和接口字段的变化节奏通常不同。

2. 定义响应对象

java 复制代码
package com.example.demo.user;

public record UserResponse(
        Long id,
        String name,
        String email,
        String status
) {
}

响应对象可以隐藏内部字段,也可以将领域对象转换成更适合客户端使用的结构。

3. 编写 Service

java 复制代码
package com.example.demo.user;

import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicLong;

@Service
public class UserService {

    private final AtomicLong idGenerator = new AtomicLong();

    public UserResponse create(CreateUserRequest request) {
        long id = idGenerator.incrementAndGet();

        return new UserResponse(
                id,
                request.name(),
                request.email(),
                "ACTIVE"
        );
    }
}

这里仍然使用内存实现,只用于演示 Spring MVC 调用链。实际项目通常会在 Service 中调用 Repository,并根据事务边界组织业务操作。

4. 编写 Controller

java 复制代码
package com.example.demo.user;

import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<UserResponse> create(
            @Valid @RequestBody CreateUserRequest request) {
        UserResponse response = userService.create(request);
        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(response);
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> findById(
            @PathVariable Long id) {
        return ResponseEntity.notFound().build();
    }
}

这里可以重点观察:

  • 请求路径由类级别和方法级别映射共同组成;
  • @RequestBody 触发消息转换器;
  • @Valid 触发参数校验;
  • ResponseEntity 控制状态码和响应体;
  • Controller 只做协议适配和服务调用。

5. 添加请求拦截器

java 复制代码
package com.example.demo.web;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;

public class TraceInterceptor implements HandlerInterceptor {

    private static final String TRACE_ID = "X-Trace-Id";

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) {

        String traceId = request.getHeader(TRACE_ID);
        if (traceId == null || traceId.isBlank()) {
            traceId = java.util.UUID.randomUUID().toString();
        }

        request.setAttribute(TRACE_ID, traceId);
        response.setHeader(TRACE_ID, traceId);
        return true;
    }
}

preHandle 返回 false 时,请求不会继续执行 Controller。权限校验失败时可以直接设置响应状态并返回 false,但要确保响应已经被正确写回。

6. 注册拦截器

java 复制代码
package com.example.demo.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TraceInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/actuator/health");
    }
}

如果拦截器本身依赖其他 Spring Bean,不建议直接使用 new 创建,而应将拦截器声明为 Bean,再通过构造方法注入依赖。

7. 添加统一异常处理

java 复制代码
package com.example.demo.web;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidation(
            MethodArgumentNotValidException exception) {

        String message = exception.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> error.getField()
                        + ": "
                        + error.getDefaultMessage())
                .collect(Collectors.joining("; "));

        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(Map.of(
                        "code", "INVALID_ARGUMENT",
                        "message", message
                ));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleUnknown(
            Exception exception) {

        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of(
                        "code", "INTERNAL_ERROR",
                        "message", "服务暂时不可用"
                ));
    }
}

真实项目中应记录 exception 的堆栈和 traceId,但对外只返回稳定的错误码和安全提示。

8. 调用并观察结果

发送合法请求:

bash 复制代码
curl -i -X POST "http://localhost:8080/api/users" -H "Content-Type: application/json" -d '{"name":"Ada","email":"ada@example.com"}'

预期得到 201:

http 复制代码
HTTP/1.1 201
Content-Type: application/json
X-Trace-Id: generated-trace-id

{"id":1,"name":"Ada","email":"ada@example.com","status":"ACTIVE"}

发送非法请求:

bash 复制代码
curl -i -X POST "http://localhost:8080/api/users" -H "Content-Type: application/json" -d '{"name":"","email":"wrong"}'

请求会在 Controller 方法执行之前校验失败,返回 400。此时如果在 Service 方法上打断点,通常不会进入。

9. 阅读源码时如何设置断点

可以按照下面的顺序设置断点:

text 复制代码
DispatcherServlet.doDispatch
  -> DispatcherServlet.getHandler
  -> DispatcherServlet.getHandlerAdapter
  -> RequestMappingHandlerAdapter.handleInternal
  -> InvocableHandlerMethod.invokeForRequest
  -> HandlerMethodArgumentResolverComposite.resolveArgument
  -> ServletInvocableHandlerMethod.invokeAndHandle
  -> HandlerMethodReturnValueHandler.handleReturnValue

不同框架版本的方法实现可能略有变化,但整体职责基本稳定。调试时先观察以下变量:

  • 当前请求 URI 和 HTTP 方法;
  • mappedHandler 是否为空;
  • HandlerMethod 的 Controller 类和方法;
  • 参数解析器选中了哪一个;
  • 返回值处理器选中了哪一个;
  • 是否进入异常解析分支。

五、常见问题与实践建议

1. 路由返回 404

常见原因:

  • Controller 不在组件扫描范围内;
  • 缺少 @RestController;
  • 类级别路径和方法级别路径拼接错误;
  • 请求方法不匹配;
  • 网关修改了路径;
  • 应用启动的上下文路径与预期不同;
  • 路由被其他配置排除。

排查顺序建议是:

text 复制代码
确认请求是否到达应用
  -> 查看已注册映射
  -> 核对 HTTP 方法和完整路径
  -> 检查组件扫描
  -> 检查网关和上下文路径

2. 路径匹配冲突

如果两个方法映射到相同路径和相同 HTTP 方法,Spring 可能在启动时报告映射冲突。

应避免依赖模糊路径:

java 复制代码
@GetMapping("/{value}")
public Object getByValue(@PathVariable String value) {
    return null;
}
java 复制代码
@GetMapping("/{id}")
public Object getById(@PathVariable Long id) {
    return null;
}

这两个路径模式对路由匹配来说可能无法明确区分。可以改成不同的固定前缀,或者使用明确的路径约束和业务命名。

3. 为什么返回 405

405 表示请求路径可能找到了处理资源,但 HTTP 方法不被支持。例如 Controller 只有 @PostMapping,客户端却使用 GET 调用。

排查时不要只看 URL,还要检查:

  • 客户端实际发送的方法;
  • 网关是否改变了方法;
  • Controller 的映射注解;
  • 是否存在重定向导致方法变化;
  • 跨域预检请求是否被正确处理。

4. 为什么返回 400

400 可能发生在 Controller 执行之前,常见原因包括:

  • @PathVariable 类型转换失败;
  • @RequestParam 缺失或格式错误;
  • JSON 语法错误;
  • Content-Type 不正确;
  • @Valid 校验失败;
  • 日期、枚举和数字转换失败。

如果断点无法进入 Controller,说明问题很可能位于参数解析和绑定阶段。

5. 415 和 406 分别是什么

415 Unsupported Media Type 通常表示服务端不接受请求体的媒体类型。重点检查请求的 Content-Type 和 Controller 的 consumes 配置。

406 Not Acceptable 通常表示服务端无法按照客户端 Accept 头要求的格式返回响应。重点检查 Accept、produces 和已注册的消息转换器。

开发环境中可以暂时打印请求头辅助排查,但生产日志要避免记录敏感认证信息。

6. 参数解析器顺序会带来什么影响

Spring MVC 会在参数解析器列表中寻找支持当前参数的解析器。如果自定义解析器的 supportsParameter 过于宽泛,可能抢先处理本应交给默认解析器的参数。

自定义解析器应:

  • 精确限制支持的参数类型或注解;
  • 避免对所有 Object 参数返回 true;
  • 明确解析失败时的异常;
  • 为不同 Spring Boot 版本编写测试;
  • 验证与其他解析器的顺序关系。

7. 为什么 @RequestBody 只能读取一次

HTTP 请求体通常是一个输入流。消息转换器读取后,后续组件不一定还能再次读取原始内容。

如果需要记录请求体,应使用经过评估的缓存包装方式,并考虑:

  • 大请求体带来的内存占用;
  • 文件上传和二进制数据;
  • 敏感字段脱敏;
  • 读取失败和编码问题;
  • 日志采样比例。

不要为了打印日志,在生产环境无条件复制所有请求体。

8. Interceptor 没有执行

检查:

  • 拦截路径是否覆盖当前请求;
  • 是否被 excludePathPatterns 排除;
  • 是否注册到了正确的 MVC 配置;
  • 请求是否在 Filter 阶段就结束;
  • 是否使用了其他 DispatcherServlet;
  • 拦截器 Bean 是否被正确创建。

如果目标是处理所有 Servlet 请求,应该考虑 Filter;Interceptor 主要面向 Spring MVC 的处理器链。

9. 全局异常处理没有生效

常见原因:

  • 异常发生在 Controller 之外;
  • 异常类型没有匹配 @ExceptionHandler;
  • Advice 不在扫描范围;
  • 异常被其他处理器提前处理;
  • 返回值无法被消息转换器序列化;
  • 异常发生在异步线程中。

不要只写一个 Exception 的兜底处理器就认为所有异常都能被统一处理。应根据异常来源和处理阶段分别测试。

10. Controller 中应该避免什么

Controller 中不建议出现:

  • 复杂 SQL;
  • 多层嵌套业务判断;
  • 事务流程;
  • 长时间阻塞的外部调用;
  • 大量重复的参数校验;
  • 直接操作线程池和底层连接;
  • 将异常堆栈直接返回客户端。

Controller 应尽量保持为协议适配层,让 Service 负责业务编排,让基础设施层负责数据库、缓存和外部服务调用。

六、进阶思考

1. 同步请求和异步请求的区别

普通 Spring MVC 请求通常由一个容器线程执行到完成:

text 复制代码
容器线程
  -> Controller
  -> 数据库或下游调用
  -> 生成响应
  -> 返回线程池

如果 Controller 长时间等待 I/O,容器线程会被占用。DeferredResult、Callable 和 WebAsyncTask 等机制可以让请求处理进入异步流程,但这不代表业务自动变成了非阻塞。

异步处理仍然需要考虑:

  • 业务线程池大小;
  • 超时;
  • 请求取消;
  • 异常传播;
  • 上下文传递;
  • 响应提交后的错误处理;
  • 应用关闭时的任务处理。

2. Spring MVC 的扩展点

常见扩展点包括:

扩展点 适合场景
Filter 所有 Servlet 请求的入口处理
HandlerInterceptor Controller 请求前后处理
WebMvcConfigurer MVC 组件和格式配置
HandlerMethodArgumentResolver 自定义方法参数
HttpMessageConverter 自定义请求和响应格式
HandlerMethodReturnValueHandler 自定义返回值处理
ControllerAdvice 统一异常和数据绑定处理
Formatter/Converter 类型转换

扩展前应先确认框架是否已经提供配置项。能通过配置解决的问题,不一定需要编写自定义扩展。

3. 请求处理链的性能分析

接口耗时可以拆成:

text 复制代码
Filter 前置处理
+ Interceptor 前置处理
+ 参数读取和反序列化
+ Controller 和 Service 执行
+ 数据库与下游调用
+ 返回值序列化
+ Interceptor 后置处理
+ Filter 后置处理

不要只统计 Controller 方法内部耗时。对于大 JSON、文件上传和大响应,还应单独观察序列化、反序列化和网络写回成本。

高并发服务中需要同时关注:

  • Tomcat 工作线程;
  • 业务线程池;
  • 数据库连接池;
  • HTTP 客户端连接池;
  • 下游服务限流;
  • 请求和连接超时。

4. 安全检查不应只放在拦截器

Interceptor 可以做登录态和基础权限检查,但资源归属和业务授权通常必须在 Service 层再次确认。

例如:

text 复制代码
拦截器:用户是否已经登录
Service:用户是否有权访问这个订单
数据库:查询是否带有正确的租户条件

如果只在入口判断"用户已登录",可能出现越权读取其他用户数据的问题。

5. 如何提高源码阅读效率

推荐使用"主线加断点"的方式:

  1. 从一个简单 Controller 开始;
  2. 在 DispatcherServlet.doDispatch 设置断点;
  3. 观察 HandlerExecutionChain;
  4. 进入 HandlerAdapter;
  5. 跟踪一个 @RequestBody 参数;
  6. 跟踪返回值如何被写成 JSON;
  7. 主动抛出一个异常,观察异常解析;
  8. 再加入拦截器,对比执行顺序。

阅读过程中只记录关键对象和职责:

text 复制代码
Request
  -> HandlerExecutionChain
  -> HandlerAdapter
  -> InvocableHandlerMethod
  -> ArgumentResolver
  -> ReturnValueHandler
  -> ExceptionResolver

这条主线掌握后,再研究初始化和自动配置会更容易。

6. Spring MVC 与 WebFlux 的边界

Spring MVC 基于 Servlet 模型,常见请求处理方式是容器线程执行同步方法;WebFlux 基于响应式编程模型,强调非阻塞和异步数据流。

选择技术时不能只看"是否异步":

  • 阻塞式数据库和客户端会抵消非阻塞收益;
  • 团队是否熟悉响应式编程很重要;
  • 调试、监控和异常处理方式有所不同;
  • 业务吞吐和延迟目标需要通过压测验证。

对于典型 CRUD 后端,Spring MVC 仍然是清晰、成熟的选择;只有当 I/O 模型、并发规模和链路特征确实适合时,才应引入响应式方案。

结论

Spring MVC 的核心任务是将 HTTP 请求转换为 Java 方法调用,再将 Java 结果转换为 HTTP 响应。DispatcherServlet 负责组织主流程,HandlerMapping 负责查找处理器,HandlerAdapter 负责执行处理器,参数解析器负责构造方法参数,消息转换器负责对象与 HTTP 内容的转换,返回值处理器和异常解析器负责完成响应。

本文的重点可以归纳为:

  • DispatcherServlet 是 Spring MVC 的统一请求入口;
  • HandlerMapping 决定调用哪个 Controller 方法;
  • HandlerAdapter 负责以统一方式执行处理器;
  • 参数解析器决定方法参数从哪里来以及如何转换;
  • 消息转换器负责 JSON 等内容的读写;
  • 拦截器属于处理器链,Filter 位于 Servlet 容器层;
  • 404、405、400、415 和 500 分别对应不同处理阶段的问题;
  • 阅读源码应抓住 doDispatch 到参数解析、返回值处理和异常解析的主线。

掌握这条请求处理链后,下一步可以继续学习 MySQL 事务、索引与锁,理解 Controller 后面的 Service、Repository 和数据库是如何共同完成一次业务请求的。

相关推荐
Sylven1 小时前
【DevOps 开发流程】什么是CI/CD?不同的阶段应当配置哪些CI/CD自动化流程?
后端
鶴哥只手遮天1 小时前
从零搭建光电仿真引擎(五):探测器成像与工程实践
后端
鶴哥只手遮天1 小时前
从零搭建光电仿真引擎(三):大气与热特性系统
后端
明华0491 小时前
保险Agent开发记录
后端
imDwAaY1 小时前
Redis List 是链表吗?从 Ziplist 到 Quicklist 揭开底层实现
redis·后端
_风不会停息1 小时前
剥开 Agent 开发:参照 pi-agent 实现一个小 Agent
人工智能·后端
sp421 小时前
Java 加解密组件再设计
java·后端
鶴哥只手遮天1 小时前
从零搭建光电仿真引擎(二):场景建模与材质系统
后端