ModelAttribute
本章与
@RequestBody形成互补。如果说@RequestBody是 RESTful API 时代处理 JSON 请求体的利器,那么@ModelAttribute就是传统表单提交时代的核心绑定机制。它负责将表单字段(application/x-www-form-urlencoded或multipart/form-data)绑定到 Java 对象,同时也承担着"请求预处理"的职责——在每次请求 Controller 方法前,向 Model 中注入公共数据。
定义与作用
@ModelAttribute 有两个截然不同的使用位置,对应两种完全不同的功能:
位置一:方法参数上——表单数据绑定
将 HTTP 请求中的表单字段绑定到 Java 对象。例如表单提交:
POST /employees
Content-Type: application/x-www-form-urlencoded
name=张三&department=技术部&salary=25000
@ModelAttribute 将这些分散的键值对组装成一个完整的 Employee 对象。
位置二:方法上——请求预处理
在 Controller 的每个请求处理方法执行前,先执行标注了 @ModelAttribute 的方法,将其返回值放入 Model 中。常用于准备下拉选项、部门列表等公共数据。
生活类比:入职登记表
想象飞翔科技的新员工入职流程:
- @RequestBody(JSON):员工直接发了一封格式规范的电子邮件(JSON),HR 打印出来即可
- @ModelAttribute(表单):员工填写了一张纸质入职登记表(HTML 表单),每个字段分散在表单的各个格子中。HR(
@ModelAttribute)需要把这些格子里的信息,按对应关系抄录到员工档案(Java 对象)中
而标注在方法上的 @ModelAttribute,就像是"每次办理入职前,HR 先把公司部门列表、职位列表打印出来放在桌上"——这些公共资料每个入职流程都需要,提前准备好可以提高效率。
核心原理
表单绑定过程
关键机制:
- WebDataBinder:数据绑定器,负责将请求参数按名称匹配到对象属性,并执行类型转换
- 无参构造器:
@ModelAttribute必须先实例化对象,因此 Java 类必须有无参构造器 - 字段名匹配:表单字段名与 Java 属性名(通过 Getter/Setter 推导)匹配,支持嵌套属性(
address.city)
方法级 @ModelAttribute 的执行时机
重要特性:
- 方法级
@ModelAttribute在当前 Controller 的每个请求处理方法之前执行 - 返回值自动放入 Model,属性名默认按返回类型首字母小写(如
List<Department>→departmentList) - 可用
@ModelAttribute("depts")显式指定 Model 中的属性名
适用位置与常用属性
| 使用位置 | 作用 | 常用属性 |
|---|---|---|
| 方法参数 | 表单数据绑定到对象 | name / value:指定 Model 中的属性名;binding:是否启用数据绑定 |
| 方法 | 请求预处理,向 Model 注入公共数据 | name / value:指定 Model 中的属性名 |
完整示例
场景
飞翔科技员工管理系统有一个"新增员工"页面,前端黄俪使用传统 HTML 表单提交(非 AJAX JSON)。表单包含姓名、部门、薪资字段。后端小崔使用 @ModelAttribute 接收表单数据。同时,每个页面都需要显示部门下拉列表,由方法级 @ModelAttribute 提前准备。
代码实现
package com.feixiang.web;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
@Controller
@RequestMapping("/employees")
public class EmployeeController {
/**
* 方法级 @ModelAttribute:每次请求前准备部门列表
*/
@ModelAttribute("departments")
public List<String> prepareDepartments() {
return Arrays.asList("技术部", "产品部", "运维部", "市场部");
}
/**
* 显示新增员工表单页面
* departments 已由 @ModelAttribute 方法注入 Model
*/
@GetMapping("/new")
public String showForm(Model model) {
model.addAttribute("employee", new Employee());
return "employee-form"; // 视图名
}
/**
* 接收表单提交:表单字段 → Employee 对象
*/
@PostMapping
public String createEmployee(@ModelAttribute Employee employee, Model model) {
model.addAttribute("message", "员工 " + employee.getName() + " 创建成功");
return "success";
}
/**
* 显式指定 Model 属性名
*/
@PostMapping("/advanced")
public String createAdvanced(@ModelAttribute("emp") Employee employee) {
return "success";
}
}
// Employee.java
package com.feixiang.web;
public class Employee {
private String name;
private String department;
private Integer salary;
public Employee() {}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDepartment() { return department; }
public void setDepartment(String department) { this.department = department; }
public Integer getSalary() { return salary; }
public void setSalary(Integer salary) { this.salary = salary; }
}
HTTP 请求示例
示例 1:表单提交(application/x-www-form-urlencoded)
curl -X POST "http://localhost:8080/employees" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=张三" \
-d "department=技术部" \
-d "salary=25000"
响应(视图渲染模式,返回 HTML 页面):
<!-- success.html -->
<!DOCTYPE html>
<html>
<head><title>操作成功</title></head>
<body>
<h1>员工 张三 创建成功</h1>
</body>
</html>
示例 2:表单字段名与属性名不匹配
curl -X POST "http://localhost:8080/employees" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userName=李四" \
-d "dept=产品部"
响应页面中 employee.name 为 null,因为表单字段 userName 与 Java 属性 name 不匹配。解决方式:统一命名,或使用 @BindParam(Spring 不提供,需自定义 PropertyEditor)。
易错场景与面试考点
误区一:@ModelAttribute 和 @RequestBody 混用场景
错误代码:
@PostMapping
public String create(@ModelAttribute @RequestBody Employee employee) {
// 错误!两者互斥
}
纠正:@ModelAttribute 和 @RequestBody 不能同时标注在同一个参数上。它们的绑定机制完全不同:
@ModelAttribute:从表单字段/查询参数绑定,使用WebDataBinder@RequestBody:从请求体读取完整 JSON/XML,使用HttpMessageConverter
选择指南:
| 场景 | 使用注解 | Content-Type |
|---|---|---|
| 传统 HTML 表单提交 | @ModelAttribute | application/x-www-form-urlencoded |
| 文件上传表单 | @ModelAttribute | multipart/form-data |
| AJAX 提交 JSON | @RequestBody | application/json |
| 简单查询参数 | @RequestParam | 任意 |
误区二:方法级 @ModelAttribute 被所有方法触发
现象:只想让 @ModelAttribute 方法在特定几个请求前执行,但它被所有方法触发了。
纠正:方法级 @ModelAttribute 默认作用于当前 Controller 的所有请求处理方法。如果只想限定特定 URL,可以结合 @RequestMapping:
@ModelAttribute
@RequestMapping("/employees/*") // 仅对 /employees/* 路径生效
public void prepareData(Model model) {
// ...
}
但更常见的做法是将公共数据准备逻辑提取到单独的 @ControllerAdvice 类中(本教程不展开 AOP 内容,仅作提示)。
误区三:忘记无参构造器
现象:@ModelAttribute Employee employee 报错,提示无法实例化。
纠正:@ModelAttribute 绑定需要先创建对象实例,再填充字段。如果 Employee 只有带参构造器,没有无参构造器,Spring 无法实例化。
解决:确保表单绑定对象有无参构造器:
public class Employee {
public Employee() {} // 必须有无参构造器
// ...
}
面试高频:@ModelAttribute 标注在方法上和参数上的区别
标准回答:
- 标注在方法上:在 Controller 的每个请求处理方法执行前运行,返回值自动放入 Model,用于准备公共数据(如下拉列表、常量配置)
- 标注在参数上:将请求参数(表单字段或查询参数)绑定到 Java 对象,用于接收客户端提交的表单数据
- 执行时机:方法级先执行,参数级在调用目标方法时执行
- 底层机制:方法级直接调用方法返回值放入 Model;参数级使用
WebDataBinder逐字段匹配绑定
小结
@ModelAttribute 是 Spring MVC 中兼具"表单绑定"和"数据预处理"双重身份的注解。标注在参数上时,它将分散的表单字段组装为完整的 Java 对象;标注在方法上时,它在每个请求前向 Model 注入公共数据。
核心要点:
- 表单绑定对象必须有无参构造器
@ModelAttribute与@RequestBody互斥,前者处理表单,后者处理 JSON- 方法级
@ModelAttribute在当前 Controller 的所有请求前执行 - 支持嵌套属性绑定(如
address.city)
本章与全局的关系:本章讲解了"表单数据的绑定与预处理"。下一节 Model 接口将讲解如何在 Controller 中向视图传递数据,这是连接"请求处理"与"视图渲染"的桥梁。