Spring MVC ModelAndView 详解

一、核心概念

1. 定义与作用

ModelAndView是 Spring MVC 框架的核心类,用于​​封装模型数据和视图信息​​。它允许控制器一次性返回:

  • ​模型数据​​:需要在视图中展示的数据

  • ​视图信息​​:指定如何渲染响应(视图名称或视图对象)

2. 设计目的

  • ​统一返回类型​​:标准化控制器方法的返回值

  • ​解耦视图技术​​:支持 JSP、Thymeleaf、FreeMarker 等多种视图技术

  • ​简化开发​​:一站式处理模型和视图

二、核心结构

复制代码
public class ModelAndView {
    private Object view;          // 视图名称或视图对象
    private ModelMap model;      // 模型数据存储
    private HttpStatus status;   // HTTP状态码(可选)
    private boolean cleared;     // 是否已清除
}

三、核心方法

1. 构造方法

复制代码
// 空构造
ModelAndView() 

// 指定视图名称
ModelAndView(String viewName) 

// 指定视图对象
ModelAndView(View view) 

// 指定视图名称和模型
ModelAndView(String viewName, Map<String, ?> model) 

// 指定视图对象和模型
ModelAndView(View view, Map<String, ?> model)

2. 关键操作方法

复制代码
// 设置视图名称
void setViewName(String viewName) 

// 获取视图名称
String getViewName() 

// 设置视图对象
void setView(View view) 

// 添加模型属性
ModelAndView addObject(String attributeName, Object attributeValue) 

// 添加多个模型属性
ModelAndView addAllObjects(Map<String, ?> modelMap) 

// 获取模型
ModelMap getModelMap() 

// 设置HTTP状态码
void setStatus(HttpStatus status)

四、使用场景与示例

1. 基本使用

复制代码
@Controller
public class ProductController {
    
    @GetMapping("/products")
    public ModelAndView listProducts() {
        ModelAndView mav = new ModelAndView("product/list"); // 视图名称
        
        // 添加模型数据
        mav.addObject("products", productService.getAllProducts());
        mav.addObject("categories", categoryService.getCategories());
        
        return mav;
    }
}

2. 链式调用(推荐)

复制代码
@GetMapping("/product/{id}")
public ModelAndView getProduct(@PathVariable Long id) {
    return new ModelAndView("product/detail")
        .addObject("product", productService.getProductById(id))
        .addObject("related", productService.getRelatedProducts(id));
}

3. 重定向

复制代码
@PostMapping("/product/create")
public ModelAndView createProduct(Product product) {
    productService.saveProduct(product);
    
    // 重定向到列表页
    return new ModelAndView("redirect:/products")
        .addObject("success", "产品创建成功");
}

4. 转发请求

复制代码
@GetMapping("/product/preview/{id}")
public ModelAndView previewProduct(@PathVariable Long id) {
    Product product = productService.getProductById(id);
    
    // 转发到详情页
    return new ModelAndView("forward:/product/" + id)
        .addObject("preview", true);
}

5. 设置HTTP状态码

复制代码
@GetMapping("/product/{id}")
public ModelAndView getProduct(@PathVariable Long id) {
    Product product = productService.getProductById(id);
    
    if (product == null) {
        return new ModelAndView("error/404")
            .setStatus(HttpStatus.NOT_FOUND);
    }
    
    return new ModelAndView("product/detail")
        .addObject("product", product)
        .setStatus(HttpStatus.OK);
}

五、内部工作机制

1. 请求处理流程

2. 模型数据传递

六、与其它返回类型对比

返回类型 优点 缺点 适用场景
​ModelAndView​ 完整封装模型和视图 支持重定向/转发 可设置状态码 代码稍显冗长 复杂视图逻辑 需要精确控制响应
​String (视图名)​ 简洁 易读 需单独处理模型 功能有限 简单页面渲染
​@ResponseBody​ 适合REST API 直接返回数据 不适用传统页面 JSON/XML API
​void​ 直接写响应 灵活性差 特殊响应处理

七、最佳实践

1. 视图命名策略

复制代码
// 使用视图解析器配置
@Bean
public ViewResolver viewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
}

// 控制器中简化视图名
@GetMapping("/users")
public ModelAndView listUsers() {
    return new ModelAndView("user/list") // 自动解析为 /WEB-INF/views/user/list.jsp
        .addObject("users", userService.getAll());
}

2. 模型数据管理

复制代码
// 使用模型工具类
public class ModelUtils {
    public static ModelAndView withSuccess(ModelAndView mav, String message) {
        return mav.addObject("success", message);
    }
    
    public static ModelAndView withError(ModelAndView mav, String error) {
        return mav.addObject("error", error);
    }
}

// 控制器中使用
@PostMapping("/order")
public ModelAndView createOrder(OrderForm form) {
    try {
        orderService.createOrder(form);
        return ModelUtils.withSuccess(
            new ModelAndView("redirect:/orders"), 
            "订单创建成功"
        );
    } catch (Exception e) {
        return ModelUtils.withError(
            new ModelAndView("order/form"),
            "创建订单失败: " + e.getMessage()
        );
    }
}

3. 响应式扩展

复制代码
// 结合Thymeleaf模板引擎
@GetMapping("/dashboard")
public ModelAndView dashboard() {
    ModelAndView mav = new ModelAndView("dashboard");
    
    // 添加复杂数据
    mav.addObject("stats", Map.of(
        "sales", salesService.getMonthlySales(),
        "users", userService.getActiveUserCount(),
        "products", productService.getTopProducts()
    ));
    
    return mav;
}

八、常见问题解决方案

1. 重定向时保留参数

复制代码
@PostMapping("/update")
public ModelAndView updateProduct(Product product) {
    productService.update(product);
    
    // 使用RedirectAttributes
    RedirectAttributes redirectAttrs = new RedirectAttributesModelMap();
    redirectAttrs.addAttribute("id", product.getId()); // URL参数
    redirectAttrs.addFlashAttribute("message", "更新成功"); // 闪存属性
    
    return new ModelAndView("redirect:/product/{id}", redirectAttrs);
}

2. 处理文件下载

复制代码
@GetMapping("/download/report")
public ModelAndView downloadReport() {
    // 使用AbstractView实现
    return new ModelAndView(new AbstractView() {
        @Override
        protected void renderMergedOutputModel(
            Map<String, Object> model, 
            HttpServletRequest request, 
            HttpServletResponse response
        ) throws Exception {
            // 设置响应头
            response.setContentType("application/pdf");
            response.setHeader("Content-Disposition", "attachment; filename=report.pdf");
            
            // 生成并写入PDF
            byte[] pdf = reportService.generatePdfReport();
            response.getOutputStream().write(pdf);
        }
    });
}

3. 国际化支持

复制代码
@GetMapping("/home")
public ModelAndView home(Locale locale) {
    ModelAndView mav = new ModelAndView("home");
    
    // 添加本地化消息
    mav.addObject("welcomeMsg", messageSource.getMessage("welcome", null, locale));
    mav.addObject("currentDate", new Date());
    
    return mav;
}

九、在 REST 中的应用

1. 返回 HTML 片段

复制代码
@GetMapping("/product/{id}/detail")
public ModelAndView productDetail(@PathVariable Long id) {
    return new ModelAndView("fragments/product-detail")
        .addObject("product", productService.getProductById(id));
}

2. 错误页面处理

复制代码
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ProductNotFoundException.class)
    public ModelAndView handleProductNotFound(ProductNotFoundException ex) {
        return new ModelAndView("error/product-not-found")
            .addObject("productId", ex.getProductId())
            .setStatus(HttpStatus.NOT_FOUND);
    }
}

十、总结

1. 核心价值

  • ​统一封装​​:模型和视图的一体化容器

  • ​灵活控制​​:支持重定向、转发、状态码设置

  • ​技术解耦​​:独立于具体视图技术

2. 适用场景

  • 传统服务端渲染应用

  • 需要精确控制HTTP响应的场景

  • 复杂页面包含多个数据源

  • 需要重定向或转发的操作

3. 最佳实践建议

  • 使用链式调用简化代码

  • 结合视图解析器简化视图路径

  • 对复杂模型使用工具类封装

  • 重定向时使用RedirectAttributes

  • 合理使用HTTP状态码增强API语义

ModelAndView 是 Spring MVC 中处理传统 Web 页面的核心组件,虽然现代开发中 REST API 和前端框架更流行,但在需要服务端渲染的场景下,它仍然是高效可靠的解决方案。