Spring Boot 从“会用”到“精通”:Model-Map原理

Model / Map / ModelMap / ModelAndView 原理

一、是什么 ------ 这些到底是什么?

1. 问题引入

在 Controller 方法中,我们经常这样写:

java 复制代码
// 源码位置:springboot2-master/boot-05-web-01/.../controller/RequestController.java
@GetMapping("/params")
public String testParam(Map<String, Object> map,
                        Model model,
                        HttpServletRequest request) {
    map.put("hello", "world666");           // Map 存数据
    model.addAttribute("world", "hello666"); // Model 存数据
    request.setAttribute("message", "HelloWorld"); // Request 存数据
    return "forward:/success";               // 转发到另一个方法
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg,
                   HttpServletRequest request) {
    // 这里能拿到前面放进去的数据!
    Object hello = request.getAttribute("hello");   // → "world666"
    Object world = request.getAttribute("world");   // → "hello666"
    Object message = request.getAttribute("message"); // → "HelloWorld"
    // 它们都在 HttpServletRequest 的属性里!
}

问题

  • MapModel 是不同类型的参数,为什么存进去的数据最终都在 Request 域中?
  • 它们之间是什么关系?存到 Map 里的数据,Model 里能看到吗?

2. 一句话答案

MapModelModelMap 类型的参数,在 Controller 方法内部指向的是同一个 BindingAwareModelMap 对象 。在视图渲染阶段,Spring 把这个 Model 中的所有数据通过 request.setAttribute() 复制到 Request 域中。


二、为什么 ------ 为什么这样设计?

1. 统一数据模型

Controller 方法可以声明不同类型的数据容器参数(Map / Model / ModelMap),但底层需要一个统一的、共享的数据载体。这样无论开发者用哪种方式存数据,最终都能被视图层拿到。

2. 兼容不同开发习惯

  • 有人习惯用原生 Map("我就当它是一个 HashMap")
  • 有人习惯用 Model(语义更明确:"这是一个数据模型")
  • 有人习惯用 ModelMap(兼容旧代码)

Spring 不强制你选哪个------底层都是同一个对象


三、怎么做 ------ 源码机制深度拆解

第一阶段:参数解析(统一到同一个对象)

1. Map 类型参数 → MapMethodProcessor
java 复制代码
// MapMethodProcessor
public boolean supportsParameter(MethodParameter parameter) {
    return Map.class.isAssignableFrom(parameter.getParameterType());
}

public Object resolveArgument(...) {
    Assert.state(mavContainer != null, "ModelAndViewContainer is required");
    return mavContainer.getModel();  // ★ 返回 mavContainer 中的共享 Model
}
2. Model 类型参数 → ModelMethodProcessor
java 复制代码
// ModelMethodProcessor
public boolean supportsParameter(MethodParameter parameter) {
    return Model.class.isAssignableFrom(parameter.getParameterType());
}

public Object resolveArgument(...) {
    Assert.state(mavContainer != null, "ModelAndViewContainer is required");
    return mavContainer.getModel();  // ★ 同样返回 mavContainer 中的共享 Model
}

关键发现 :两个不同的解析器,核心都是 mavContainer.getModel()

2. 共享数据模型:BindingAwareModelMap

mavContainer.getModel() 返回的到底是什么?

java 复制代码
// ModelAndViewContainer 内部定义
private final ModelMap defaultModel = new BindingAwareModelMap();

public ModelMap getModel() {
    // ...
    return this.defaultModel;
}

BindingAwareModelMap 的继承关系:

复制代码
LinkedHashMap<String, Object>          ← 本质是一个 Map!
    ↑
ModelMap                                ← 实现了 Model 接口
    ↑
BindingAwareModelMap                    ← Spring MVC 的实际类型

核心结论

Controller 方法中的 MapModelModelMap 参数,都指向 ModelAndViewContainer 内部同一个 BindingAwareModelMap 实例。

这意味着 :你通过 Map 存的数据,Model 能看见;通过 Model 存的数据,Map 也能看见。它们是同一块内存的不同"视角"。

第二阶段:Controller 执行与数据填充

getMethodArgumentValues() 返回参数数组后,doInvoke(args) 反射执行 Controller 方法。

方法执行期间,所有对传入 Map / Model 的修改(put / addAttribute)都直接写入共享的 BindingAwareModelMap

第三阶段:封装 ModelAndView

Controller 执行完后,RequestMappingHandlerAdapter.invokeHandlerMethod() 调用 getModelAndView() 封装结果:

java 复制代码
// RequestMappingHandlerAdapter.getModelAndView()
modelFactory.updateModel(webRequest, mavContainer);  // 处理 Session 属性 + 数据绑定结果
ModelMap model = mavContainer.getModel();             // 取出共享 Model
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, ...);
return mav;

此时 ModelAndView 内部持有完整的 ModelMap

第四阶段:视图渲染 ------ 数据进入 Request 域

回到 DispatcherServlet.processDispatchResult()render()

java 复制代码
// 1. 解析视图名 → 找到具体的 View 对象
View view = this.resolveViewName(viewName, mv.getModelInternal(), locale, request);

// 2. 调用 View 渲染
view.render(mv.getModelInternal(), request, response);

view.render() 内部(AbstractView 或其子类),执行关键的数据转移操作

java 复制代码
// AbstractView.exposeModelAsRequestAttributes()
model.forEach((name, value) -> {
    // ★ 核心操作:使用 Servlet API 将数据写入 Request 域
    request.setAttribute(name, value);
});

这就是"为什么 Model/Map 中的数据最终会在 Request 域中"的终极答案。


四、完整四阶段流转图

复制代码
┌─────────────────────────────────────────────────┐
│  阶段一:参数解析(统一)                        │
│  MapMethodProcessor / ModelMethodProcessor       │
│       ↓ 调用                                    │
│  mavContainer.getModel()                        │
│       ↓ 返回                                    │
│  BindingAwareModelMap(同一个实例)               │
├─────────────────────────────────────────────────┤
│  阶段二:Controller 执行(填充)                  │
│  map.put(...) / model.addAttribute(...)          │
│       ↓ 写入                                    │
│  共享的 BindingAwareModelMap                     │
├─────────────────────────────────────────────────┤
│  阶段三:封装 ModelAndView(传递)                │
│  getModelAndView()                              │
│       ↓                                         │
│  new ModelAndView(viewName, modelMap)           │
├─────────────────────────────────────────────────┤
│  阶段四:视图渲染(暴露到 Request 域)             │
│  view.render(model, request, response)          │
│       ↓                                         │
│  exposeModelAsRequestAttributes()                │
│       ↓                                         │
│  request.setAttribute(name, value) ← 转移完成!   │
└─────────────────────────────────────────────────┘

五、项目源码佐证

RequestController 中,我们有一个跨转发的数据传递测试:

java 复制代码
// 源码位置:springboot2-master/boot-05-web-01/.../controller/RequestController.java

// 生产者:往 Map 和 Model 里分别放数据
@GetMapping("/params")
public String testParam(Map<String, Object> map, Model model, HttpServletRequest request) {
    map.put("hello", "world666");     // 存入共享 Model
    model.addAttribute("world", "hello666"); // 同样存入共享 Model
    request.setAttribute("message", "HelloWorld"); // 直接放入 Request
    return "forward:/success";  // 转发
}

// 消费者:验证数据是否都在 Request 域中
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute("msg") String msg, HttpServletRequest request) {
    Object hello = request.getAttribute("hello");     // ← map.put 的,拿得到!
    Object world = request.getAttribute("world");     // ← model.addAttribute 的,拿得到!
    Object message = request.getAttribute("message"); // ← request.setAttribute 的,拿得到!
    // 三种方式存的数据全部都在 Request 域中
}

六、总结

核心点 说明
共享模型 MapModelModelMap 参数都指向同一个 BindingAwareModelMap 实例
解析器 MapMethodProcessor(Map)、ModelMethodProcessor(Model)
数据载体 ModelAndViewContainer.defaultModel(类型为 BindingAwareModelMap
数据共享 Controller 内部 Map 和 Model 数据互通(同一块内存)
Request 暴露 视图渲染阶段 exposeModelAsRequestAttributes() 执行 request.setAttribute()
继承链 BindingAwareModelMapModelMapLinkedHashMap(本质是 Map)
相关推荐
buhuizhiyuci4 分钟前
【算法篇】位运算 —— 基础篇
java·数据库·算法
卷无止境16 分钟前
Python itertools 盘点真正用得上的20个常用方法
后端
laboratory agent开发25 分钟前
企业AI Agent落地前,先回答四个工程问题
java·前端·人工智能
微三云 - 廖会灵 (私域系统开发)25 分钟前
电商系统国际化架构设计:多语言、多币种、多时区、多税制的全链路实现
java·前端·数据库
XS03010626 分钟前
Spring AI 第二课-流式输出 & 运行时动态参数配置
java·人工智能·spring
额恩6631 分钟前
阶段五:HttpOnly Cookie 登录持久化
java·spring
万亿少女的梦16832 分钟前
基于微信小程序、Spring Boot与Vue3的智慧养老管理系统设计与实现
spring boot·redis·微信小程序·mybatis·vue3
芒鸽36 分钟前
HarmonyOS ArkUI Marquee 跑马灯与 Gauge 仪表盘:公告滚动与数据展示
后端
Gopher_HBo39 分钟前
镜像服务门面模式
后端
一缕清烟在人间42 分钟前
CalendarYearPage 年视图:12 个月微型日历网格
后端