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 的属性里!
}
问题:
Map和Model是不同类型的参数,为什么存进去的数据最终都在 Request 域中?- 它们之间是什么关系?存到 Map 里的数据,Model 里能看到吗?
2. 一句话答案
Map、Model、ModelMap类型的参数,在 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 方法中的
Map、Model、ModelMap参数,都指向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 域中
}
六、总结
| 核心点 | 说明 |
|---|---|
| 共享模型 | Map、Model、ModelMap 参数都指向同一个 BindingAwareModelMap 实例 |
| 解析器 | MapMethodProcessor(Map)、ModelMethodProcessor(Model) |
| 数据载体 | ModelAndViewContainer.defaultModel(类型为 BindingAwareModelMap) |
| 数据共享 | Controller 内部 Map 和 Model 数据互通(同一块内存) |
| Request 暴露 | 视图渲染阶段 exposeModelAsRequestAttributes() 执行 request.setAttribute() |
| 继承链 | BindingAwareModelMap → ModelMap → LinkedHashMap(本质是 Map) |