摘要
在 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 需要完成很多工作:
- 找到可以处理这个请求的 Controller 方法;
- 判断请求方法和路径是否匹配;
- 从路径中解析 id;
- 从查询参数中解析 detail;
- 将字符串转换成 Long 和 boolean;
- 调用 Controller 方法;
- 将返回的 Java 对象转换为 JSON;
- 设置状态码和响应头;
- 如果中间出现异常,将异常转换成响应。
如果只把这些能力归因于"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
);
}
}
实际源码还会处理异步请求、文件上传、请求属性恢复和结果清理等情况,但主线可以归纳为:
- 准备请求;
- 找 Handler;
- 找 HandlerAdapter;
- 调用 Handler;
- 处理返回值;
- 处理异常;
- 完成响应。
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);
}
大致过程是:
- 识别参数上存在 @RequestBody;
- 根据请求 Content-Type 选择消息转换器;
- 从请求体读取字节;
- 反序列化为 CreateUserRequest;
- 如果存在 @Valid,执行 Bean Validation;
- 校验失败则抛出参数校验异常;
- 校验成功后将对象传给 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. 如何提高源码阅读效率
推荐使用"主线加断点"的方式:
- 从一个简单 Controller 开始;
- 在 DispatcherServlet.doDispatch 设置断点;
- 观察 HandlerExecutionChain;
- 进入 HandlerAdapter;
- 跟踪一个 @RequestBody 参数;
- 跟踪返回值如何被写成 JSON;
- 主动抛出一个异常,观察异常解析;
- 再加入拦截器,对比执行顺序。
阅读过程中只记录关键对象和职责:
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 和数据库是如何共同完成一次业务请求的。