跳到正文

从 Node.js 到 Spring MVC(2):控制器参数从哪里来

从 Node.js 到 Spring MVC

  1. 应用入口与页面请求
  2. 控制器参数从哪里来(当前)
  3. 表单绑定、校验与视图
  4. Bean 容器与构造器注入
  5. 沿着新增 Owner 读完整请求
  6. 用 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,就能还原这次调用前的数据准备过程。