跳到正文

从 Node.js 到 Spring MVC(3):表单绑定、校验与视图

从 Node.js 到 Spring MVC

  1. 应用入口与页面请求
  2. 控制器参数从哪里来
  3. 表单绑定、校验与视图(当前)
  4. Bean 容器与构造器注入
  5. 沿着新增 Owner 读完整请求
  6. 用 MockMvc 验证框架行为

Petclinic 提交 Owner 表单后,字段无效时会重新展示 HTML,响应状态仍可为 200。这与 JSON API 常见的 400 或 FastAPI 的 422 不同,但它是一个明确的页面交互流程:保留输入,显示字段错误,让用户继续修改。

本文基于 500158f73241。Owner 同时是持久化实体和表单模型,项目没有为这条页面流程再拆一个 OwnerCreate DTO。先理解现有结构,再讨论它的边界。

表单数据如何变成 Owner

templates/owners/createOrUpdateOwnerForm.html 中的 <form> 使用 th:object="${owner}",方法是 post。页面字段按 firstName、lastName、address、city、telephone 等名称提交,浏览器常规表单提交时编码成请求参数。MVC 找到 Model 中的 owner,再通过数据绑定填入它的属性。

这里不是 Jackson 把 JSON 转成实体。把请求改成 application/json 并发送同名字段,不能保证这条无 @RequestBody 的处理方法按同样方式读取。协议、Content-Type 和参数声明必须一起看。

Controller 还配置了绑定限制:

@InitBinder
	public void setAllowedFields(WebDataBinder dataBinder) {
		dataBinder.setDisallowedFields("id", "*.id");
	}

方法虽然名叫 setAllowedFields,实际调用的是 setDisallowedFields,禁止通过绑定写入 id 和嵌套 id。这针对的是客户端批量赋值,不影响服务端代码在校验后调用 setId()。不能只根据方法名描述安全规则,更不能说实体上所有属性都自动不可信地开放或自动受保护。

模型准备、字段绑定和校验分别参与表单处理

@Valid 与 BindingResult 配合

创建方法接收 @Valid Owner owner, BindingResult result。@Valid 触发 Bean Validation,BindingResult 接收紧邻模型的绑定和校验错误。这里需要相邻,不能随意在两者之间插入其他参数。模型属性绑定与校验解释了这种处理约定。

Owner 的字段约束分布在 Owner 和父类 Person。例如 firstName、lastName 使用 @NotBlank 与 @Size(max = 30);city 长度最多 80;telephone 使用十位数字模式。@Column(length = 80) 表达持久化映射信息,@Size(max = 80) 才是这里提前给用户反馈的 Bean Validation 约束。数据库列限制与表单校验不是同一阶段。

字段为空、绑定不能转换或超长时,result.hasErrors() 可以成立。方法返回表单视图,模板读取同一个模型和错误信息。因为错误已被业务方法接住并选择页面,MVC 不必把它自动变成统一 JSON 错误。

视图名、Model 和真正的 HTML

控制器返回字符串只指定视图。Model 保存 Owner、错误和页面数据;Thymeleaf 根据模板生成最终 HTML。模型对象不是原封不动发送给浏览器的 JSON,页面实际公开什么取决于模板引用了哪些字段。

同一个控制器的三类返回结果由 MVC 继续解释

详情方法还返回 ModelAndView:指定 owners/ownerDetails,再 addObject(owner)。这把视图和模型放进一个返回值。与 Express 相比,相当于同时给出模板名和模板数据,只是 MVC 负责最后调用渲染过程。

如果将来另开 JSON API,再为输入和输出设计 DTO,会更容易限制字段、稳定接口。但不能仅仅把 @Controller 换成 @RestController 就认定改造完成:原有返回的模板名会变成文本内容,重定向与表单错误处理也需要重新设计。

总结

这条表单链路先准备 Owner,再绑定请求参数、执行校验,最后由控制器选择继续渲染还是重定向。字段限制、Bean Validation、数据库约束和模板输出各管一段边界。理解它们发生的位置,才能解释为什么相同的无效输入在页面应用里会返回带错误提示的 200 HTML。