ModelAndView
本章紧接
ViewResolver,讲解 Controller 如何同时携带"视图名"和"数据"。前面章节中,我们使用Model参数传递数据、String返回值指定视图名——这种方式简洁但不够灵活。ModelAndView将两者封装为一个对象,适合需要动态决定视图名或一次性组装完整响应的场景。
定义与作用
ModelAndView 是 Spring MVC 提供的视图-数据复合类,同时携带:
- 视图信息:视图名(String)或 View 对象
- 模型数据:键值对形式的属性集合
当 Controller 方法需要根据业务逻辑动态选择视图时,ModelAndView 是最佳选择。例如:
- 普通用户访问返回
"user-dashboard" - 管理员访问返回
"admin-dashboard" - 数据相同,但展示页面不同
生活类比:定制礼盒
想象飞翔科技的年终福利发放:
- Model + String 返回值:行政部把礼品清单(Model 数据)交给统一包装部,包装部总是用同一种礼盒(固定视图名)
- ModelAndView:行政部根据员工级别,直接把礼品装进不同档次的礼盒——普通员工用标准礼盒(
"standard-box"),优秀员工用豪华礼盒("premium-box""),但里面的礼品清单(数据)是一样的
ModelAndView 让"装什么"和"用什么盒子"在同一个地方决定,避免了分散控制。
核心原理
ModelAndView 组装过程
与 Model + String 的对比:
| 维度 | Model + String 返回值 | ModelAndView |
|---|---|---|
| 视图名指定 | 方法返回值 | setViewName() |
| 数据传递 | model.addAttribute() | addObject() |
| 动态视图 | 困难(返回值只能一个) | 容易(运行时决定) |
| 代码分散度 | 视图名在 return 语句,数据在方法体 | 视图名和数据集中在一个对象 |
| 链式操作 | Model 支持 | ModelAndView 支持 |
| 典型场景 | 视图名固定的常规页面 | 视图名动态决定的复杂场景 |
适用位置与常用方法
ModelAndView 作为 Controller 方法的返回值 使用。
| 方法 | 说明 |
|---|---|
setViewName(String) | 设置逻辑视图名 |
setView(View) | 直接设置 View 对象 |
addObject(String, Object) | 添加模型属性 |
addAllObjects(Map) | 批量添加模型属性 |
getModel() | 获取底层 ModelMap |
getViewName() | 获取视图名 |
完整示例
场景
飞翔科技员工管理系统的详情页面需要根据用户角色显示不同视图:普通员工看到简化版("employee-simple"),部门经理看到完整版("employee-full")。后端小崔使用 ModelAndView 动态决定视图名。
代码实现
package com.feixiang.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import java.util.Arrays;
import java.util.List;
@Controller
@RequestMapping("/employees")
public class EmployeeController {
private final List<Employee> employees = Arrays.asList(
new Employee(1L, "张三", "技术部", 25000, "ENGINEER"),
new Employee(2L, "李四", "产品部", 22000, "MANAGER")
);
/**
* 动态视图:根据角色返回不同页面
*/
@GetMapping("/{id}")
public ModelAndView getEmployee(@PathVariable Long id,
@RequestParam String role) {
Employee employee = employees.stream()
.filter(e -> e.getId().equals(id))
.findFirst()
.orElse(new Employee());
ModelAndView mav = new ModelAndView();
// 动态决定视图名
if ("MANAGER".equals(role)) {
mav.setViewName("employee-full");
} else {
mav.setViewName("employee-simple");
}
// 添加模型数据
mav.addObject("employee", employee);
mav.addObject("role", role);
return mav;
}
/**
* 链式构建 ModelAndView
*/
@GetMapping
public ModelAndView list() {
return new ModelAndView("employee-list")
.addObject("employees", employees)
.addObject("totalCount", employees.size());
}
/**
* 同时设置 View 对象(跳过 ViewResolver)
*/
@GetMapping("/direct")
public ModelAndView directView() {
// 实际项目中可注入自定义 View
// mav.setView(new CustomPdfView());
ModelAndView mav = new ModelAndView();
mav.setViewName("employee-list");
mav.addObject("message", "直接指定视图");
return mav;
}
}
// Employee.java
package com.feixiang.web;
public class Employee {
private Long id;
private String name;
private String department;
private Integer salary;
private String role;
public Employee() {}
public Employee(Long id, String name, String department, Integer salary, String role) {
this.id = id;
this.name = name;
this.department = department;
this.salary = salary;
this.role = role;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
HTTP 请求示例
示例 1:普通员工查看(简化视图)
curl "http://localhost:8080/employees/1?role=ENGINEER"
响应(渲染 employee-simple.html):
<!DOCTYPE html>
<html>
<head><title>员工信息</title></head>
<body>
<h1>员工简介</h1>
<p>姓名: 张三</p>
<p>部门: 技术部</p>
<!-- 简化版不显示薪资 -->
</body>
</html>
示例 2:经理查看(完整视图)
curl "http://localhost:8080/employees/1?role=MANAGER"
响应(渲染 employee-full.html):
<!DOCTYPE html>
<html>
<head><title>员工详情</title></head>
<body>
<h1>员工完整档案</h1>
<p>ID: 1</p>
<p>姓名: 张三</p>
<p>部门: 技术部</p>
<p>薪资: 25000</p>
<p>角色: ENGINEER</p>
</body>
</html>
示例 3:链式构建(员工列表)
curl "http://localhost:8080/employees"
响应(渲染 employee-list.html):
<!DOCTYPE html>
<html>
<head><title>员工列表</title></head>
<body>
<h1>员工列表</h1>
<p>总人数: 2</p>
<table>...</table>
</body>
</html>
易错场景与面试考点
误区一:ModelAndView 和 @ResponseBody 混用
错误代码:
@GetMapping("/test")
@ResponseBody
public ModelAndView test() {
return new ModelAndView("view").addObject("key", "value");
}
结果:@ResponseBody 会尝试将 ModelAndView 对象序列化为 JSON,客户端收到的是 {"view":null,"model":{...}},而不是渲染后的 HTML。
纠正:ModelAndView 用于视图渲染模式,不能与 @ResponseBody 或 @RestController 同时使用。
误区二:视图名拼写错误导致 404
现象:mav.setViewName("employe-list")(拼写错误,少了一个 e),客户端收到 404。
排查:ViewResolver 按 "employe-list" 查找模板文件,找不到匹配的文件。检查视图名拼写,确保与模板文件名完全一致。
误区三:试图在 ModelAndView 中设置 HTTP 状态码
现象:需要在返回 ModelAndView 的同时设置 HTTP 状态码 201。
纠正:ModelAndView 本身不支持设置状态码。需要完整控制响应时,应使用 ResponseEntity 配合 ResponseBody 渲染(或使用 @ResponseStatus 注解,本教程不展开)。
// 需要状态码 + 视图渲染的折中方案
@ResponseStatus(HttpStatus.CREATED)
@GetMapping("/created")
public ModelAndView created() {
return new ModelAndView("success").addObject("msg", "创建成功");
}
面试高频:ModelAndView vs Model + String 返回值
标准回答:
- Model + String:视图名固定,通过方法返回值指定。代码简洁,适合大多数常规场景
- ModelAndView:视图名可在运行时动态决定,将视图名和数据封装在一个对象中。适合 A/B 测试、权限分级展示、条件渲染等场景
- 选择建议:默认使用
Model+String,仅在需要动态视图名时使用ModelAndView - 底层等价:两者最终都会被 DispatcherServlet 拆解为视图名 + Model 数据,走相同的 ViewResolver 解析流程
小结
ModelAndView 是 Spring MVC 中同时携带视图名和模型数据的复合类。它将 Controller 的"返回什么数据"和"用什么视图展示"两个决策集中在一个对象中,特别适合视图名需要动态决定的场景。
核心要点:
ModelAndView同时封装视图名和 Model 数据- 支持链式构建:
new ModelAndView("view").addObject("key", value) - 不能与
@ResponseBody/@RestController混用 - 默认优先使用
Model+String,需要动态视图时才用ModelAndView
本章与全局的关系:本章讲解了"如何动态决定视图并传递数据"。下一节 forward 与 redirect 将讲解两种特殊的视图转发机制——服务器内部转发和客户端重定向,以及它们各自的使用场景和差异。