乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

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 返回值

标准回答:

  1. Model + String:视图名固定,通过方法返回值指定。代码简洁,适合大多数常规场景
  2. ModelAndView:视图名可在运行时动态决定,将视图名和数据封装在一个对象中。适合 A/B 测试、权限分级展示、条件渲染等场景
  3. 选择建议:默认使用 Model + String,仅在需要动态视图名时使用 ModelAndView
  4. 底层等价:两者最终都会被 DispatcherServlet 拆解为视图名 + Model 数据,走相同的 ViewResolver 解析流程

小结

ModelAndView 是 Spring MVC 中同时携带视图名和模型数据的复合类。它将 Controller 的"返回什么数据"和"用什么视图展示"两个决策集中在一个对象中,特别适合视图名需要动态决定的场景。

核心要点:

  • ModelAndView 同时封装视图名和 Model 数据
  • 支持链式构建:new ModelAndView("view").addObject("key", value)
  • 不能与 @ResponseBody / @RestController 混用
  • 默认优先使用 Model + String,需要动态视图时才用 ModelAndView

本章与全局的关系:本章讲解了"如何动态决定视图并传递数据"。下一节 forward 与 redirect 将讲解两种特殊的视图转发机制——服务器内部转发和客户端重定向,以及它们各自的使用场景和差异。

上一页
ResponseEntity
下一页
ViewResolver