从 Node.js 到 Spring MVC(2):控制器参数从哪里来
- 应用入口与页面请求
- 控制器参数从哪里来(当前)
- 表单绑定、校验与视图
- Bean 容器与构造器注入
- 沿着新增 Owner 读完整请求
- 用 MockMvc 验证框架行为
Express 的处理函数通常固定接收 req、res、next,Koa 则接收 ctx。Spring MVC 的方法可以直接声明页码、Owner、BindingResult 和 Model。一组参数解析器根据注解、类型和方法上下文,为这次调用准备实参。参数名在部分规则中参与匹配,但需要和这些声明一起看。
本文以 Petclinic 500158f73241 的 OwnerController 为例。这是处理 HTML 页面和表单的 MVC 控制器,没有把 Owner 默认当作 JSON body。
先读签名,再找值的来源
查询入口的签名是:
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner,
BindingResult result, Model model, RedirectAttributes redirectAttributes)
这里只展示声明,完整方法在 OwnerController。每个参数承担不同职责:
| 声明 | 本例来源 | Node.js 中可借用的理解 |
|---|---|---|
@RequestParam ... int page |
请求参数,缺省为 1,并做 int 转换 | 读取并转换 req.query.page |
Owner owner |
Model 中的 Owner,加上请求参数绑定 | 构造表单对象,再填查询或表单值 |
BindingResult result |
紧邻 Owner 的绑定、校验结果 | 已准备的字段错误集合 |
Model model |
MVC 提供的当前请求模型容器 | 传给 res.render() 的数据 |
RedirectAttributes |
MVC 提供的重定向属性容器 | 设置重定向 URL 参数或 flash 数据 |
@RequestParam 在 Servlet 中面对的是请求参数,包含查询和表单;这里因为是 GET 查询,直觉上可以先对照 req.query。不能把这份对照扩展成它永远只读 URL query。MVC 参数列表说明了支持的参数种类。
Owner 先从哪里取得
Controller 中另有一个 @ModelAttribute("owner") 方法:
@ModelAttribute("owner")
public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) {
return ownerId == null ? new Owner()
: this.owners.findById(ownerId)
.orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId
+ ". Please ensure the ID is correct " + "and the owner exists in the database."));
}
MVC 会在调用目标处理方法前准备这些 Model 属性。路径有 ownerId 时从 Repository 查现有对象,没有时创建新 Owner。处理方法中的 Owner 就有了可供绑定的起点。required = false 允许不存在这个路径变量,Integer 可以表示 null;若使用原始类型 int,就不能这样承接缺失值。
因此 /owners/new 的初始化方法即使没有显式写 model.addAttribute("owner", ...),模板仍然能取得 owner。对于编辑页面,返回视图名之前也已经能加载现有实体。方法名字 findOwner 本身并不是魔法,注解和返回属性名才把它加入这条流程。
这与依赖注入要分开:构造器里的 OwnerRepository 在容器创建 Controller 时提供;方法里的 Owner 则在每次请求时准备。两者都会让代码少写一次显式取值,但发生时间和生命周期不同。
类型转换失败会挡在业务之前
/owners?page=2 可以转成 int,/owners?page=abc 通常会在参数解析时发生类型转换错误,目标方法不能按正常的 int 参数继续执行。defaultValue = "1" 处理缺省或空值,不是「所有非法字符都变成 1」。
而 page=0 已经是合法整数,会进入业务方法。当前代码先把数据库分页索引约束到非负,再根据总页数判断是否重定向回第一页。这说明「能转换成类型」和「业务范围有效」是两层规则。与 Express 手写 Number() 和范围判断相比,MVC 帮忙承担了前一层,后一层仍要读业务。
也可以显式拿原始请求
MVC 支持声明 HttpServletRequest、HttpServletResponse 等参数,但不是每个处理函数都需要接收它们。只声明 Model 或路径参数,通常更能表明方法真正依赖什么。JSON 接口可以声明 @RequestBody,让消息转换器处理请求体;它与本例的模型属性绑定是另一条解析路径。
排查陌生参数时,先看显式注解;没有注解,再看它是不是 MVC 支持的特殊类型,或者默认模型属性;最后看是否有项目自定义解析器、ModelAttribute 和 Binder。不要从「参数看起来像实体」直接跳到「框架查数据库注入了它」。本项目的数据库读取明确写在 findOwner 中。
总结
Spring MVC 从请求、Model 和框架辅助对象中准备方法需要的值。显式注解指出来源,类型决定适用的解析器,BindingResult 的位置把错误关联到模型。再结合 ModelAttribute 和 Binder,就能还原这次调用前的数据准备过程。