springmvc处理模型数据
Spring MVC 模型数据处理详解:4 种核心方式与错误处理
在 Spring MVC 开发中,将业务数据传递到视图(如 JSP、Thymeleaf)是核心需求之一。Spring MVC 提供了 ModelAndView、Map/Model、@SessionAttributes、@ModelAttribute 四种灵活的模型数据处理方式,覆盖从 “单次请求数据传递” 到 “跨请求数据共享” 的全场景。从 “数据传递原理→使用场景→实战示例” 三个维度,彻底讲透模型数据的处理逻辑。
核心概念:模型数据的本质
Spring MVC 中的 “模型数据” 本质是 键值对(Key-Value),存储在 Model 或 ModelMap 中,最终会通过 HttpServletRequest.setAttribute() 绑定到请求域(Request Scope),视图技术(如 JSP 的 EL 表达式 ${key})可直接访问这些数据。
- 作用域分类:
- 请求域(Request Scope):数据仅在当前请求有效(默认,如 ModelAndView、Map 传递的数据);
- 会话域(Session Scope):数据在当前用户会话中有效(需通过
@SessionAttributes显式设置)
方式 1:ModelAndView —— 视图与数据的统一载体
ModelAndView 是 Spring MVC 早期最常用的模型数据处理方式,同时封装 “视图信息” 和 “模型数据”,适合需要明确指定视图且传递数据的场景。
1. 核心原理与源码解析
ModelAndView 内部维护两个核心变量,分别对应 “视图” 和 “数据”:
public class ModelAndView {
// 视图:支持两种类型——1. 逻辑视图名(String,如 "hello");2. 具体 View 对象(如 InternalResourceView)
private Object view;
// 模型数据:本质是 ModelMap(继承自 LinkedHashMap,有序存储键值对)
private ModelMap model;
// ------------------------------ 视图相关方法 ------------------------------
// 设置逻辑视图名(最常用)
public void setViewName(String viewName) {
this.view = viewName;
}
// 获取逻辑视图名
public String getViewName() {
return this.view instanceof String ? (String) this.view : null;
}
// ------------------------------ 模型数据相关方法 ------------------------------
// 获取模型(懒加载,第一次调用时初始化 ModelMap)
public ModelMap getModelMap() {
if (this.model == null) {
this.model = new ModelMap();
}
return this.model;
}
// 添加模型数据(键值对)
public ModelAndView addObject(String attributeName, Object attributeValue) {
this.getModelMap().addAttribute(attributeName, attributeValue);
return this; // 链式调用,支持连续 addObject
}
// 重载方法:省略键名,Spring 自动生成键名(如 User 对象→键名 "user")
public ModelAndView addObject(Object attributeValue) {
this.getModelMap().addAttribute(attributeValue);
return this;
}
}
- 底层绑定逻辑:Spring MVC 会将
ModelAndView中的模型数据,通过request.setAttribute(key, value)绑定到请求域,视图渲染时可直接访问。
2. 实战示例
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.Date;
public class ModelAndViewController {
public ModelAndView testModelAndView() {
// 1. 创建 ModelAndView,指定逻辑视图名(对应视图路径:/WEB-INF/views/userList.jsp)
ModelAndView mav = new ModelAndView("userList");
// 2. 添加模型数据(键值对)
mav.addObject("title", "用户列表"); // 字符串类型
mav.addObject("currentTime", new Date()); // 日期类型
mav.addObject(new User(1, "张三", 25)); // 自定义对象(自动生成键名 "user")
// 3. 返回 ModelAndView(Spring MVC 自动解析视图并渲染数据)
return mav;
}
// 自定义 User 类
static class User {
private Integer id;
private String name;
private Integer age;
// 构造器、getter/setter、toString()
public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}
}
视图渲染(JSP 示例):
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <body> <h1>${title}</h1> <!-- 输出:用户列表 --> <p>当前时间:${currentTime}</p> <!-- 输出:当前日期时间 --> <p>用户信息:${user.name}(${user.age}岁)</p> <!-- 输出:张三(25岁) --> </body> </html>
方式 2:Map/Model/ModelMap —— 方法入参传递数据
Spring MVC 支持将 Map、Model、ModelMap 作为 Controller 方法的入参,方法执行时会自动注入这些对象,添加的数据会被 Spring 自动封装到模型中,无需手动创建 ModelAndView。
1. 三种类型的关系与区别
| 类型 | 继承关系 | 核心特点 | 适用场景 |
|---|---|---|---|
Map |
JDK 原生接口(java.util.Map) |
最简洁,仅支持基本的键值对操作 | 简单数据传递,无需额外功能 |
Model |
Spring 接口(org.springframework.ui.Model) |
提供 addAttribute() 等方法,方法名更语义化 |
推荐使用,Spring 官方推荐 |
ModelMap |
继承 LinkedHashMap,实现 Model |
兼具 Map 的灵活性和 Model 的语义化方法,支持排序 | 需要有序存储或额外方法(如 clear()) |
- 底层实现:无论使用哪种类型,Spring MVC 实际注入的都是
BindingAwareModelMap(ModelMap的子类),三种类型的使用效果完全一致,仅 API 风格不同。
2. 实战示例
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
public class ModelController {
// 1. 使用 Map 作为入参
public String testMap(Map<String, Object> map) {
map.put("message", "使用 Map 传递数据");
map.put("count", 100);
return "result"; // 返回逻辑视图名,模型数据自动传递
}
// 2. 使用 Model 作为入参(推荐)
public String testModel(Model model) {
model.addAttribute("message", "使用 Model 传递数据");
model.addAttribute("user", new User(2, "李四", 30)); // 自动生成键名 "user"
return "result";
}
// 3. 使用 ModelMap 作为入参
public String testModelMap(ModelMap modelMap) {
modelMap.addAttribute("message", "使用 ModelMap 传递数据");
modelMap.put("date", new Date()); // 支持 Map 的 put 方法
return "result";
}
}
视图渲染(Thymeleaf 示例):
<html xmlns:th="http://www.thymeleaf.org"> <body> <p th:text="${message}"></p> <!-- 输出对应方法传递的消息 --> <p th:text="${count != null ? '数量:' + count : ''}"></p> <!-- 仅 testMap 方法有值 --> <p th:text="${user != null ? '用户名:' + user.name : ''}"></p> <!-- 仅 testModel 方法有值 --> </body> </html>
方式 3:@SessionAttributes —— 跨请求数据共享
@SessionAttributes 是 类级别的注解,用于将 “请求域中的模型数据” 提升到 “会话域(HttpSession)”,实现多个请求之间的数据共享(如用户登录信息、跨页面表单数据)。
1. 核心原理
- 标注位置:仅能标注在
@Controller类上,作用于该类的所有方法; - 数据提升逻辑:当 Controller 方法向模型中添加
@SessionAttributes指定的键名数据时,Spring MVC 会自动将该数据同步到HttpSession; - 数据销毁:需手动调用
SessionStatus.setComplete()销毁会话域中的数据(否则数据会一直保存在 Session 中,直到 Session 过期)。
2. 实战示例
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import javax.servlet.http.HttpSession;
// 类级别注解:将模型中键名为 "user" 和 "orderId" 的数据存入 Session
public class SessionAttributesController {
// 1. 第一次请求:向模型添加数据,自动同步到 Session
public String login(Model model) {
// 添加键名为 "user" 的数据(会被 @SessionAttributes 同步到 Session)
model.addAttribute("user", new User(1, "张三", "admin"));
// 添加键名为 "orderId" 的数据(同样同步到 Session)
model.addAttribute("orderId", "ORDER_20240520");
return "loginSuccess"; // 视图可访问请求域和会话域的 user、orderId
}
// 2. 第二次请求:从 Session 中获取共享数据
public String showUser(HttpSession session, Model model) {
// 方式1:直接从 HttpSession 获取
User sessionUser = (User) session.getAttribute("user");
System.out.println("从 Session 获取用户:" + sessionUser.getName()); // 输出:张三
// 方式2:从 Model 获取(Spring 自动从 Session 同步到 Model)
User modelUser = (User) model.getAttribute("user");
System.out.println("从 Model 获取用户:" + modelUser.getName()); // 输出:张三
return "userInfo";
}
// 3. 销毁 Session 中的共享数据
public String logout(SessionStatus sessionStatus) {
// 标记 Session 数据为完成,Spring 会清除 @SessionAttributes 管理的 Session 数据
sessionStatus.setComplete();
return "logoutSuccess";
}
}
- 注意事项:
- 避免滥用:
@SessionAttributes管理的 Session 数据需手动销毁,否则会占用内存,建议仅用于短期共享的数据; - 键名匹配:仅模型中键名与
@SessionAttributes一致的数据才会被同步到 Session。
- 避免滥用:
方式 4:@ModelAttribute —— 预置模型数据或绑定请求参数
@ModelAttribute 是 灵活的注解,可标注在 “方法” 或 “方法参数” 上,分别实现 “预置模型数据” 和 “绑定请求参数到模型” 的功能。
1. 标注在方法上:预置模型数据
- 作用:在当前 Controller 的所有方法执行前,先执行被
@ModelAttribute标注的方法,将其返回值或添加的数据存入模型,实现 “数据预置”(如全局查询字典数据、用户信息)。 - 执行时机:被
@ModelAttribute标注的方法,会在当前 Controller 的每个请求处理方法(如@GetMapping、@PostMapping方法)执行前调用。
实战示例:预置全局字典数据
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.stereotype.Controller;
import java.util.HashMap;
import java.util.Map;
public class ModelAttributeController {
// 1. 预置模型数据:每个请求处理方法执行前都会调用该方法
public void initDict(Model model) {
// 添加字典数据(所有视图都可访问)
Map<String, String> genderDict = new HashMap<>();
genderDict.put("1", "男");
genderDict.put("2", "女");
model.addAttribute("genderDict", genderDict);
// 添加当前登录用户(模拟)
model.addAttribute("loginUser", new User(1, "管理员"));
}
// 2. 请求处理方法:可直接使用 initDict 预置的数据
public String showForm() {
// 无需手动添加 genderDict 和 loginUser,视图已可访问
return "form";
}
// 3. 另一个请求处理方法:同样可访问预置数据
public String showUser() {
return "userInfo";
}
}
视图渲染:
<!-- 直接访问 预置的数据 --> <select name="gender"> <option value="1">${genderDict['1']}</option> <!-- 输出:男 --> <option value="2">${genderDict['2']}</option> <!-- 输出:女 --> </select> <p>当前登录:${loginUser.name}</p> <!-- 输出:管理员 -->
2. 标注在方法参数上:绑定请求参数到模型
- 作用:将请求参数自动绑定到方法参数对象,并将该对象存入模型(无需手动
model.addAttribute()),适合表单提交场景。 - 绑定逻辑:Spring MVC 会根据 “请求参数名” 与 “对象属性名” 的匹配关系,自动填充属性值(如请求参数
name=张三绑定到User对象的name属性)。
实战示例:表单提交与参数绑定
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.stereotype.Controller;
public class FormController {
// 表单提交:@ModelAttribute 自动绑定请求参数到 User 对象,并存入模型
public String submitUser( User user) {
// 1. user 对象已自动填充请求参数(如 name、age)
System.out.println("提交的用户:" + user.getName());
// 2. 无需手动 addAttribute,user 已存入模型(键名 "user")
return "userResult";
}
}
// User 类(属性名与请求参数名一致)
class User {
private String name;
private Integer age;
// 无参构造器(必须,Spring 反射创建对象)、getter/setter
}
表单页面(JSP):
<form action="/submitUser" method="post"> 姓名:<input type="text" name="name" /> <!-- 参数名与 User 属性名一致 --> 年龄:<input type="number" name="age" /> <button type="submit">提交</button> </form>结果页面:
<p>提交成功!</p> <p>姓名:${user.name}</p> <!-- 输出提交的姓名 --> <p>年龄:${user.age}</p> <!-- 输出提交的年龄 -->
模型数据绑定错误处理:BindingResult 与 Errors
当模型数据绑定或校验失败时(如参数类型不匹配、校验注解不满足),Spring MVC 会将错误信息封装到 BindingResult 或 Errors 对象中,开发者可通过这些对象处理错误。
1. 核心依赖:JSR-380 校验注解
需先添加校验依赖(如 Hibernate Validator),使用 @NotNull、@NotBlank 等注解标注模型属性:
<!-- Maven 依赖 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>
2. 实战示例:校验与错误处理
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.stereotype.Controller;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
// 带校验的模型类
class UserForm {
// 非空校验
private String name;
// 非null校验
private Integer age;
// 无参构造器、getter/setter
}
public class ValidationController {
// @Validated:触发校验;BindingResult:接收校验错误(必须紧跟在被校验参数后)
public String submitForm(
UserForm userForm,
BindingResult bindingResult) {
// 1. 判断是否存在绑定/校验错误
if (bindingResult.hasErrors()) {
// 2. 处理错误(如返回原表单页面,显示错误信息)
return "form"; // 错误信息会自动存入模型,视图可访问
}
// 3. 校验通过,执行业务逻辑
return "success";
}
}
视图显示错误(Thymeleaf 示例):
<form th:action="@{/submitForm}" method="post"> <!-- 姓名输入框 + 错误提示 --> <div> <label>姓名:</label> <input type="text" th:field="${userForm.name}" /> <!-- 显示姓名的错误信息 --> <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color:red"></span> </div> <!-- 年龄输入框 + 错误提示 --> <div> <label>年龄:</label> <input type="number" th:field="${userForm.age}" /> <!-- 显示年龄的错误信息 --> <span th:if="${#fields.hasErrors('age')}" th:errors="*{age}" style="color:red"></span> </div> <button type="submit">提交</button> </form>
四种模型数据方式对比与选择建议
| 方式 | 核心优势 | 适用场景 | 数据作用域 |
|---|---|---|---|
| ModelAndView | 统一封装视图和数据,逻辑清晰 | 需要明确指定视图且传递数据 | 请求域(默认) |
| Map/Model/ModelMap | 轻量灵活,无需创建额外对象 | 仅传递数据,视图通过返回值指定 | 请求域(默认) |
| @SessionAttributes | 跨请求数据共享 | 用户登录信息、跨页面表单数据 | 会话域 |
| @ModelAttribute | 预置全局数据或自动绑定请求参数 | 全局字典数据、表单参数绑定 | 请求域(默认) |
选择建议
- 简单数据传递:优先使用
Model(语义化好,Spring 推荐); - 明确指定视图:使用
ModelAndView(适合早期非注解式开发,现在较少用); - 跨请求共享:使用
@SessionAttributes(注意手动销毁数据); - 全局数据预置或表单绑定:使用
@ModelAttribute; - 数据校验:必须配合
@Validated和BindingResult。
总结
Spring MVC 模型数据处理的核心是 “将数据绑定到合适的作用域”,通过四种方式覆盖不同场景需求:
- 单次请求数据:
Model、ModelAndView; - 跨请求数据:
@SessionAttributes; - 全局预置或表单绑定:
@ModelAttribute; - 错误处理:
BindingResult