2096 字
约 6 分钟
14
S07-X08 · Spring MVC 的请求映射与参数绑定

请求进 DispatcherServlet 后,先找到该由哪个方法处理,再把 HTTP 报文塞进方法参数。这两步看着是框架义务,实际上每一步都能让接口在线上返回 400、404、406 或 415,而堆栈里只有一行 MethodArgumentNotValidException,甚至什么都没有。

下面按"匹配 → 绑定 → 转换 → 返回"四段拆,每段说清可选面、默认值和坑。


1. @RequestMapping 的匹配维度

维度 属性 示例 不匹配的结果
路径 value / path /orders/{id} 404
方法 method RequestMethod.GET 405
参数 params params = "type=paid" 400
头 headers headers = "X-Api-Version=2" 404
请求体类型 consumes application/json 415
响应类型 produces application/json 406
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
    public OrderVO get(@PathVariable Long id) { return orderService.get(id); }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
    public Long create(@RequestBody CreateCmd cmd) { return orderService.create(cmd); }

    @GetMapping(params = "type=paid")
    public List<OrderVO> paid() { return orderService.paid(); }
}

多个映射命中时按"具体程度"排优先级:精确路径 > 含路径变量的模式 > 通配,同级再比方法、params、headers 的约束数量,带正则的 {id:[0-9]+} 比纯 {id} 更具体。两个同等具体的映射会直接启动失败并报 Ambiguous mapping——比运行时随机选一个强。


2. 路径变量与正则

写法 说明
/orders/{id} 基础形态,类型由方法参数决定
/orders/{id:[0-9]+} 正则约束,不匹配就走别的映射或 404
/orders/{id}/items/{itemId} 多变量,名字要和 @PathVariable 对上
/files/** 尾部通配,只能出现在末尾
@GetMapping("/orders/{id:[0-9]{1,18}}")
public OrderVO byId(@PathVariable("id") Long id) { }

@GetMapping({"/orders/current", "/orders/{id}"})   // 字面优先于变量
public OrderVO either(@PathVariable(required = false) Long id) { }

@PathVariable 默认 required = true,而映射里声明的路径变量不可能缺失,所以这个属性只在"多个模式共享一个方法"时有意义。用 required = false 时参数必须是包装类,不能是 long,否则缺失时绑不了 null。


3. 参数绑定来源注解

注解 取哪里 编码/格式 典型误用
@RequestParam query string 或 application/x-www-form-urlencoded body 字符串再转换 用在 JSON body 上,取不到
@PathVariable URI 模板 同上 路径含 . 被截断
@RequestBody 整个请求体 HttpMessageConverter 用于表单,报 415
@RequestHeader 请求头 字符串转换 大小写、多值头
@CookieValue Cookie 字符串转换 Cookie 名含特殊字符
@ModelAttribute 表单字段/查询参数 逐字段 setter 每次请求都执行
@PostMapping(value = "/search", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public PageResult<OrderVO> search(@RequestParam(defaultValue = "1") int page,
                                  @RequestParam(required = false) OrderStatus status,
                                  @ModelAttribute OrderQuery query) { }

@RequestBody 一个方法里只能出现一次(整条流被读完);@RequestParam 与它共用是允许的,但前者只从 query string 取,JSON body 里的同名字段取不到——第 9 节那条坑由此而来。


4. @RequestBody 与表单提交的分界

差别不在注解,在 Content-Type 与转换器的对应关系。

请求 Content-Type 可用注解 转换器
application/json @RequestBody MappingJackson2HttpMessageConverter
application/x-www-form-urlencoded @RequestParam / @ModelAttribute FormHttpMessageConverter
multipart/form-data @RequestPart / @ModelAttribute MultipartHttpMessageConverter

consumes 与真实头不匹配时 Spring 直接回 415,根本不进你的方法。常见于前端用默认 JSON 提交而后端写了 consumes = form-urlencoded,或接口写了 @RequestBody 但前端发了 FormData。

# 415 的现场复现
curl -i -X POST http://[HOST]/api/orders \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     -d 'sku=A1&qty=2'
# HTTP/1.1 415 Unsupported Media Type

5. 日期与枚举的绑定

类型 默认行为 显式指定
LocalDate 依赖 spring.mvc.format.date,未配置时按 ISO @DateTimeFormat(iso = DATE)
Date 依赖注册的 Converter,容易随环境变化 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
枚举 按 name 匹配,大小写敏感 自定义 Converter 或用 @JsonCreator
Boolean true/false,1/0 视情况 显式 Converter
@InitBinder
public void init(WebDataBinder binder) {          // 只影响本 Controller
    binder.registerCustomEditor(Date.class,
        new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}
public enum OrderStatus {
    CREATED, PAID, SHIPPED;

    @JsonCreator
    public static OrderStatus of(String v) {      // 只对 @RequestBody 生效
        return OrderStatus.valueOf(v.toUpperCase());
    }
}

@JsonCreator 只对 @RequestBody 的 Jackson 反序列化生效;@RequestParam OrderStatus 走 StringToEnumConverterFactory,不在 Jackson 里,要另注册 Converter。这就是枚举绑定"时好时坏"的原因。


6. @ModelAttribute 的回填与副作用

@ModelAttribute 标注的方法在每次该 Controller 处理请求前都会执行,然后把结果放进 Model。

@Controller
public class OrderPageController {

    @ModelAttribute("statuses")
    public OrderStatus[] statuses() {        // 每个请求都跑一次
        return OrderStatus.values();
    }

    @GetMapping("/orders")
    public String list(@ModelAttribute OrderQuery query, Model model) {
        model.addAttribute("page", orderService.search(query));
        return "order/list";
    }
}

两个副作用:一是它在参数绑定之前执行,不可能依赖请求参数(除非参数是 @RequestParam);二是每次请求都执行,里面查库等于给所有接口加一次查询——缓存或 binding = false 都不解决,只能挪出这个方法。


7. 返回值的四种处理

返回 处理方式 说明
@ResponseBody 标注的方法返回对象 HttpMessageConverter 序列化 @RestController 等于类级 @ResponseBody
ResponseEntity<T> 状态码 + 头 + body 全控 需要 201、Location 头时用
String(无 @ResponseBody) 视图名 与 @RestController 混用时极易搞错
void 认为已自行写入响应 什么都不写会得到 200 空体
@PostMapping
public ResponseEntity<Long> create(@RequestBody CreateCmd cmd) {
    Long id = orderService.create(cmd);
    return ResponseEntity.created(URI.create("/api/orders/" + id)).body(id);
}

@RestController 里想返回视图名,必须去掉类级 @ResponseBody,否则 "order/list" 会被当字符串写进响应体。


8. 内容协商与 406

produces 与客户端 Accept 头做匹配,匹配不上就是 406。

场景 结果
produces = application/json,客户端 Accept: */* 200
produces = application/json,客户端 Accept: text/html 406
未写 produces,Jackson 在类路径上 默认返回 JSON
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true
      parameter-name: format     # /api/orders?format=xml,慎用

用参数做内容协商会污染语义,也容易变成缓存键噪声。需要多格式时优先用不同路径或 Accept 头。


9. HandlerMethodArgumentResolver 扩展点

参数来源不在标准注解里时(比如从 token 解出当前用户),别在方法体里手工解析,加个解析器:

public class CurrentUserResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter p) {
        return p.hasParameterAnnotation(CurrentUser.class);
    }
    @Override
    public Object resolveArgument(MethodParameter p, ModelAndViewContainer m,
                                  NativeWebRequest req, WebDataBinderFactory b) {
        return userService.parse(req.getHeader("Authorization"));   // 失败抛可映射 401 的异常
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserResolver());
    }
}

注意这套配置对 WebFlux 无效,是两套扩展点。


10. 排错表

现象 原因 处理
415 consumes 与实际 Content-Type 不符 改前端头,或改 consumes
406 produces 与 Accept 不符 补 produces 或让客户端放宽 Accept
路径变量含 . 被截断(a.b 只剩 a) 尾部路径匹配 + 后缀模式 关闭后缀匹配,或改用 query 传参
URL 与 body 同名字段只取到一个 两者来源不同,@RequestParam 只读 query 明确拆成两个不同参数名
GET 请求用 @RequestBody 失败 GET 语义无 body,部分客户端不发 改 @ModelAttribute 或 query 参数
返回视图名却输出字符串 类上有 @ResponseBody 拆 Controller 或改返回 ModelAndView

11. 边界

场景 建议
简单 CRUD 注解足够,别上自定义解析器
参数来自 token / 签名 用 HandlerMethodArgumentResolver
十几个可选查询条件 用 @ModelAttribute 包一层查询对象
文件 + JSON 混合上传 multipart + @RequestPart

一句话:映射决定"谁处理",consumes/produces 决定"能否进",参数注解决定"从哪取"。分开看,4xx 基本一次定位。


12. 自测

  1. 接口返回 415,最可能改哪一边:consumes 还是 Accept?
  2. 路径变量 {id:[0-9]+} 不匹配时,最终是 404 还是 400?
  3. @RequestParam 和 @RequestBody 同时出现,能从 body 取到值吗?
  4. 枚举作为 @RequestParam 时按什么匹配?和 @RequestBody 的差别在哪?
  5. @ModelAttribute 标注的方法什么时候执行?为什么可能拖慢所有接口?
  6. @RestController 里返回 "order/list",会发生什么?
S07-X08 · Spring MVC 的请求映射与参数绑定
https://www.clxhxhhr.top/posts/5855/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。