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)
相关推荐
程序猿乐锅1 小时前
【苍穹外卖|Day02】后台接口自测闭环:Token、DTO 与 yml 配置
java·开发语言
心之伊始1 小时前
Spring Boot Actuator + Micrometer 自定义业务指标:不只是健康检查
java·架构·源码分析·csdn
Eason_LYC1 小时前
【GetShell 实战】CVE-2026-34486 Tomcat 加密拦截器绕过:从漏洞验证到反弹 Shell 全流程
java·渗透测试·tomcat·java反序列化·rce·远程代码执行漏洞·cve-2026-34486
qq_2518364571 小时前
基于java 税务管理系统设计与实现
java·开发语言
超梦dasgg1 小时前
Java 生产环境分布式定时任务全解(实战落地版)
java·开发语言·分布式
破土士V1 小时前
Java基础知识集合
java·开发语言
一只齐刘海的猫1 小时前
【Leetcode】 接雨水
java·算法·leetcode
㳺三才人子1 小时前
初探 Flask-WTF
后端·python·flask·html5