0%

springmvc处理模型数据

Spring MVC 模型数据处理详解:4 种核心方式与错误处理

在 Spring MVC 开发中,将业务数据传递到视图(如 JSP、Thymeleaf)是核心需求之一。Spring MVC 提供了 ModelAndView、Map/Model、@SessionAttributes、@ModelAttribute 四种灵活的模型数据处理方式,覆盖从 “单次请求数据传递” 到 “跨请求数据共享” 的全场景。从 “数据传递原理→使用场景→实战示例” 三个维度,彻底讲透模型数据的处理逻辑。

核心概念:模型数据的本质

Spring MVC 中的 “模型数据” 本质是 键值对(Key-Value),存储在 ModelModelMap 中,最终会通过 HttpServletRequest.setAttribute() 绑定到请求域(Request Scope),视图技术(如 JSP 的 EL 表达式 ${key})可直接访问这些数据。

  • 作用域分类:
    1. 请求域(Request Scope):数据仅在当前请求有效(默认,如 ModelAndView、Map 传递的数据);
    2. 会话域(Session Scope):数据在当前用户会话中有效(需通过 @SessionAttributes 显式设置)

方式 1:ModelAndView —— 视图与数据的统一载体

ModelAndView 是 Spring MVC 早期最常用的模型数据处理方式,同时封装 “视图信息” 和 “模型数据”,适合需要明确指定视图且传递数据的场景。

1. 核心原理与源码解析

ModelAndView 内部维护两个核心变量,分别对应 “视图” 和 “数据”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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. 实战示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.util.Date;

@Controller
public class ModelAndViewController {

@GetMapping("/testModelAndView")
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 示例):

    1
    2
    3
    4
    5
    6
    7
    8
    <%@ 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 实际注入的都是 BindingAwareModelMapModelMap 的子类),三种类型的使用效果完全一致,仅 API 风格不同。

2. 实战示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;

@Controller
public class ModelController {

// 1. 使用 Map 作为入参
@GetMapping("/testMap")
public String testMap(Map<String, Object> map) {
map.put("message", "使用 Map 传递数据");
map.put("count", 100);
return "result"; // 返回逻辑视图名,模型数据自动传递
}

// 2. 使用 Model 作为入参(推荐)
@GetMapping("/testModel")
public String testModel(Model model) {
model.addAttribute("message", "使用 Model 传递数据");
model.addAttribute("user", new User(2, "李四", 30)); // 自动生成键名 "user"
return "result";
}

// 3. 使用 ModelMap 作为入参
@GetMapping("/testModelMap")
public String testModelMap(ModelMap modelMap) {
modelMap.addAttribute("message", "使用 ModelMap 传递数据");
modelMap.put("date", new Date()); // 支持 Map 的 put 方法
return "result";
}
}
  • 视图渲染(Thymeleaf 示例):

    1
    2
    3
    4
    5
    6
    7
    8
    <!DOCTYPE html>
    <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. 核心原理

  1. 标注位置:仅能标注在 @Controller 类上,作用于该类的所有方法;
  2. 数据提升逻辑:当 Controller 方法向模型中添加 @SessionAttributes 指定的键名数据时,Spring MVC 会自动将该数据同步到 HttpSession
  3. 数据销毁:需手动调用 SessionStatus.setComplete() 销毁会话域中的数据(否则数据会一直保存在 Session 中,直到 Session 过期)。

2. 实战示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
@Controller
@RequestMapping("/session")
@SessionAttributes({"user", "orderId"})
public class SessionAttributesController {

// 1. 第一次请求:向模型添加数据,自动同步到 Session
@GetMapping("/login")
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 中获取共享数据
@GetMapping("/showUser")
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 中的共享数据
@GetMapping("/logout")
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 方法)执行前调用。
实战示例:预置全局字典数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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;

@Controller
@RequestMapping("/modelAttr")
public class ModelAttributeController {

// 1. 预置模型数据:每个请求处理方法执行前都会调用该方法
@ModelAttribute
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 预置的数据
@GetMapping("/showForm")
public String showForm() {
// 无需手动添加 genderDict 和 loginUser,视图已可访问
return "form";
}

// 3. 另一个请求处理方法:同样可访问预置数据
@GetMapping("/showUser")
public String showUser() {
return "userInfo";
}
}
  • 视图渲染:

    1
    2
    3
    4
    5
    6
    <!-- 直接访问 @ModelAttribute 预置的数据 -->
    <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 属性)。
实战示例:表单提交与参数绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.stereotype.Controller;

@Controller
public class FormController {

// 表单提交:@ModelAttribute 自动绑定请求参数到 User 对象,并存入模型
@PostMapping("/submitUser")
public String submitUser(@ModelAttribute("user") 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)

    1
    2
    3
    4
    5
    <form action="/submitUser" method="post">
    姓名:<input type="text" name="name" /> <!-- 参数名与 User 属性名一致 -->
    年龄:<input type="number" name="age" />
    <button type="submit">提交</button>
    </form>
  • 结果页面

    1
    2
    3
    <p>提交成功!</p>
    <p>姓名:${user.name}</p> <!-- 输出提交的姓名 -->
    <p>年龄:${user.age}</p> <!-- 输出提交的年龄 -->

模型数据绑定错误处理:BindingResult 与 Errors

当模型数据绑定或校验失败时(如参数类型不匹配、校验注解不满足),Spring MVC 会将错误信息封装到 BindingResultErrors 对象中,开发者可通过这些对象处理错误。

1. 核心依赖:JSR-380 校验注解

需先添加校验依赖(如 Hibernate Validator),使用 @NotNull@NotBlank 等注解标注模型属性:

1
2
3
4
5
6
<!-- Maven 依赖 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.5.Final</version>
</dependency>

2. 实战示例:校验与错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 {
@NotBlank(message = "姓名不能为空") // 非空校验
private String name;

@NotNull(message = "年龄不能为空") // 非null校验
private Integer age;

// 无参构造器、getter/setter
}

@Controller
public class ValidationController {

// @Validated:触发校验;BindingResult:接收校验错误(必须紧跟在被校验参数后)
@PostMapping("/submitForm")
public String submitForm(
@Validated @ModelAttribute("userForm") UserForm userForm,
BindingResult bindingResult) {

// 1. 判断是否存在绑定/校验错误
if (bindingResult.hasErrors()) {
// 2. 处理错误(如返回原表单页面,显示错误信息)
return "form"; // 错误信息会自动存入模型,视图可访问
}

// 3. 校验通过,执行业务逻辑
return "success";
}
}
  • 视图显示错误(Thymeleaf 示例):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <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 预置全局数据或自动绑定请求参数 全局字典数据、表单参数绑定 请求域(默认)

选择建议

  1. 简单数据传递:优先使用 Model(语义化好,Spring 推荐);
  2. 明确指定视图:使用 ModelAndView(适合早期非注解式开发,现在较少用);
  3. 跨请求共享:使用 @SessionAttributes(注意手动销毁数据);
  4. 全局数据预置或表单绑定:使用 @ModelAttribute
  5. 数据校验:必须配合 @ValidatedBindingResult

总结

Spring MVC 模型数据处理的核心是 “将数据绑定到合适的作用域”,通过四种方式覆盖不同场景需求:

  • 单次请求数据:ModelModelAndView
  • 跨请求数据:@SessionAttributes
  • 全局预置或表单绑定:@ModelAttribute
  • 错误处理:BindingResult

欢迎关注我的其它发布渠道