Spring MVC 的请求链路
Spring MVC 做的事,是把一次 HTTP 请求翻译成一次 Java 方法调用,再把返回值翻译成 HTTP 响应。理解这条链路,比记住几个注解更重要。
一、DispatcherServlet 是入口
Spring MVC 的核心不是 @RestController,而是 DispatcherServlet。它接住请求,再交给后面的组件处理。
一次请求大致经过这些步骤:
HandlerMapping根据 URL、HTTP 方法、请求头等条件找到处理方法。HandlerAdapter负责调用这个方法,并把参数准备好。HandlerMethodArgumentResolver解析@PathVariable、@RequestParam、@RequestBody等参数。- Controller 执行完,返回值交给
HandlerMethodReturnValueHandler。 - 返回值需要写 JSON 时,由
HttpMessageConverter完成序列化;需要页面时,由ViewResolver找到视图。
注解的作用是把方法登记进这条链路,而不是替代链路。@GetMapping 不会自己接请求,真正接请求的还是 DispatcherServlet。
请求进入 DispatcherServlet 后,主要组件按下面的顺序协作。

从 Filter 到 MessageConverter,每一段都在处理同一件事的不同侧面:请求如何进入、方法如何被选中、参数如何绑定、响应如何写出。
二、一个最小请求
java
// 示意:Controller 只负责 HTTP 参数和返回值
@RestController
@RequestMapping("/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
Order get(@PathVariable Long id) {
return orderService.get(id);
}
}
这段代码把路径里的 id 交给 OrderService,Controller 不碰业务规则。@RestController 等于 @Controller 加 @ResponseBody,所以返回值会直接写进响应体。
如果换成 @Controller,返回的字符串默认会被当成视图名。想直接返回数据,就要在方法或类上补 @ResponseBody。这个差别经常导致"接口返回了一个页面名",而不是 JSON。
三、参数和返回值
常见参数来源可以这样分:
@PathVariable取路径变量,例如/orders/42里的42。@RequestParam取查询参数,例如?page=2。@RequestBody取请求体,通常配合 JSON 使用。@RequestHeader取请求头。
@RequestBody 背后会读取请求输入流。这个流通常只能读一次,所以不要在过滤器或拦截器里先读一遍,再让 Controller 读第二遍。前后两处都想拿请求体时,应该先包装成可重复读取的请求对象。
返回值处理也要看上下文。返回对象并加上 @ResponseBody 时,Spring 会寻找能把对象写成 JSON 的消息转换器;类路径里有 Jackson 时,通常会用它。返回 ResponseEntity 可以同时控制状态码、响应头和响应体,适合需要精确控制 HTTP 响应的接口。
四、404、400、415
接口报错时,先按 HTTP 语义定位:
| 现象 | 常见原因 | 先查什么 |
|---|---|---|
| 404 | URL 没有匹配到处理方法 | 映射路径、请求方法、组件扫描范围 |
| 400 | 参数类型转换失败、校验失败 | @PathVariable / @RequestParam 类型、校验注解 |
| 415 | 请求体格式和接口要求不一致 | Content-Type、@RequestBody、消息转换器 |
| 500 | Controller 或 Service 内部异常 | 异常栈、全局异常处理、下游依赖 |
404 不一定是 Controller 没写对。Controller 所在的包如果不在启动类的扫描范围里,映射根本不会注册。415 也不一定是 JSON 写错,先确认请求头是不是 application/json,再看消息转换器有没有加载。
五、Controller 的边界
Controller 的职责是协议适配:接参数、调用 Service、组织响应。业务规则不要塞进来,事务也不要直接开在 Controller 里。
java
// 示意:把业务规则留在 Service
@RestController
@RequestMapping("/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
ResponseEntity<Order> create(@RequestBody CreateOrderRequest request) {
Order order = orderService.create(request);
return ResponseEntity.status(201).body(order);
}
}
这段代码只做参数接收和响应状态设置。订单能不能创建、库存怎么扣、价格怎么算,都属于 Service。
Filter、Interceptor 和 Controller 也不在同一层。Filter 在 Servlet 容器层处理请求,Interceptor 在 Spring MVC 找到 Handler 之后执行,Controller 才是具体的方法。登录校验、编码、跨域这类通用逻辑,先判断应该放在哪一层,不要全堆进 Controller。
六、排错顺序
遇到接口问题时,可以按这个顺序走:
- 请求有没有进入
DispatcherServlet。 HandlerMapping有没有找到方法。- 参数解析器有没有拿到正确的值。
- Controller 有没有正常调用 Service。
- 返回值有没有被正确的消息转换器写出。
404 先查映射和扫描范围,415 先查 Content-Type 和消息转换器,400 先查参数绑定。把问题留在 MVC 层解决,不要一看到接口失败就去翻 AOP。