每篇一句

一个开源的技术产品做得好不好,主要是看你能解决多少非功能性问题(因为功能性问题是所有产品都能够想到的)

前言

写这篇文章非我本意,因为我觉得对如题的这个几个类的了解还是比较基础且简单的一块内容,直到有超过两个同学问过我一些问题的时候:通过聊天发现小伙伴都听说过这几个类,但对于他们的使用、功能定位是傻傻分不清楚的(因为名字上都有很多的相似之处)。

那么书写本文就是当作一篇科普类文章记录下来,已经非常熟悉小伙伴就没太大必要往下继续阅读本文内容了,因为这块不算难的(当然我只是建议而已~)。

ModelAndViewContainer

我把这个类放在首位,是因为相较而言它的逻辑性稍强一点,并且对于理解处理器ReturnValue返回值的处理上有很好的帮助。

ModelAndViewContainer:可以把它定义为ModelAndView上下文的容器,它承担着整个请求过程中的数据传递工作-->保存着ModelView。官方doc对它的解释是这句话:

Records model and view related decisions made by {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} and
{@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers} during the course of invocation of a controller method.

翻译成"人话"便是:记录HandlerMethodArgumentResolverHandlerMethodReturnValueHandler在处理Controller的handler方法时 使用的模型model和视图view相关信息.。

当然它除了保存ModelView外,还额外提供了一些其它功能。下面我们先来熟悉熟悉它的API、源码:

// @since 3.1
public class ModelAndViewContainer {
// =================它所持有的这些属性还是蛮重要的=================
// redirect时,是否忽略defaultModel 默认值是false:不忽略
private boolean ignoreDefaultModelOnRedirect = false;
// 此视图可能是个View,也可能只是个逻辑视图String
@Nullable
private Object view;
// defaultModel默认的Model
// 注意:ModelMap 只是个Map而已,但是实现类BindingAwareModelMap它却实现了org.springframework.ui.Model接口
private final ModelMap defaultModel = new BindingAwareModelMap();
// 重定向时使用的模型(提供set方法设置进来)
@Nullable
private ModelMap redirectModel;
// 控制器是否返回重定向指令
// 如:使用了前缀"redirect:xxx.jsp"这种,这个值就是true。然后最终是个RedirectView
private boolean redirectModelScenario = false;
// Http状态码
@Nullable
private HttpStatus status; private final Set<String> noBinding = new HashSet<>(4);
private final Set<String> bindingDisabled = new HashSet<>(4); // 很容易想到,它和@SessionAttributes标记的元素有关
private final SessionStatus sessionStatus = new SimpleSessionStatus();
// 这个属性老重要了:标记handler是否**已经完成**请求处理
// 在链式操作中,这个标记很重要
private boolean requestHandled = false;
... public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
public void setView(@Nullable Object view) {
this.view = view;
}
// 是否是视图的引用
public boolean isViewReference() {
return (this.view instanceof String);
} // 是否使用默认的Model
private boolean useDefaultModel() {
return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
} // 注意子方法和下面getDefaultModel()方法的区别
public ModelMap getModel() {
if (useDefaultModel()) { // 使用默认视图
return this.defaultModel;
} else {
if (this.redirectModel == null) { // 若重定向视图为null,就new一个空的返回
this.redirectModel = new ModelMap();
}
return this.redirectModel;
}
}
// @since 4.1.4
public ModelMap getDefaultModel() {
return this.defaultModel;
} // @since 4.3 可以设置响应码,最终和ModelAndView一起被View渲染时候使用
public void setStatus(@Nullable HttpStatus status) {
this.status = status;
} // 以编程方式注册一个**不应**发生数据绑定的属性,对于随后声明的@ModelAttribute也是不能绑定的
// 虽然方法是set 但内部是add哦 ~~~~
public void setBindingDisabled(String attributeName) {
this.bindingDisabled.add(attributeName);
}
public boolean isBindingDisabled(String name) {
return (this.bindingDisabled.contains(name) || this.noBinding.contains(name));
}
// 注册是否应为相应的模型属性进行数据绑定
public void setBinding(String attributeName, boolean enabled) {
if (!enabled) {
this.noBinding.add(attributeName);
} else {
this.noBinding.remove(attributeName);
}
} // 这个方法需要重点说一下:请求是否已在处理程序中完全处理
// 举个例子:比如@ResponseBody标注的方法返回值,无需View继续去处理,所以就可以设置此值为true了
// 说明:这个属性也就是可通过源生的ServletResponse、OutputStream来达到同样效果的
public void setRequestHandled(boolean requestHandled) {
this.requestHandled = requestHandled;
}
public boolean isRequestHandled() {
return this.requestHandled;
} // =========下面是Model的相关方法了==========
// addAttribute/addAllAttributes/mergeAttributes/removeAttributes/containsAttribute
}

直观的阅读过源码后,至少我能够得到如下结论,分享给大家:

  • 它维护了模型model:包括defaultModleredirectModel
  • defaultModel是默认使用的Model,redirectModel是用于传递redirect时的Model
  • Controller处理器入参写了Model或ModelMap类型时候,实际传入的是defaultModel

    - defaultModel它实际是BindingAwareModel,是个Map。而且继承了ModelMap又实现了Model接口,所以在处理器中使用ModelModelMap时,其实都是使用同一个对象~~~

    - 可参考MapMethodProcessor,它最终调用的都是mavContainer.getModel()方法
  • 若处理器入参类型是RedirectAttributes类型,最终传入的是redirectModel

    - 至于为何实际传入的是defaultModel??参考:RedirectAttributesMethodArgumentResolver,使用的是new RedirectAttributesModelMap(dataBinder)
  • 维护视图view(兼容支持逻辑视图名称)
  • 维护是否redirect信息,及根据这个判断HandlerAdapter使用的是defaultModel或redirectModel
  • 维护@SessionAttributes注解信息状态
  • 维护handler是否处理标记(重要)

下面我主要花笔墨重点介绍一下它的requestHandled这个属性的作用:

requestHandled属性

1、首先看看isRequestHandled()方法的使用:

RequestMappingHandlerAdaptermavContainer.isRequestHandled()方法的使用,或许你就能悟出点啥了:

这个方法的执行实际是:HandlerMethod完全调用执行完成后,就执行这个方法去拿ModelAndView了(传入了request和ModelAndViewContainer

RequestMappingHandlerAdapter:
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
// 将列为@SessionAttributes的模型属性提升到会话
modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
} ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
// 真正的View 可见ModelMap/视图名称、状态HttpStatus最终都交给了Veiw去渲染
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
} // 这个步骤:是Spring MVC对重定向的支持~~~~
// 重定向之间传值,使用的RedirectAttributes这种Model~~~~
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
}

可以看到如果ModelAndViewContainer 已经被处理过,此处直接返回null,也就是不会再继续处理Model和View了~

2、setRequestHandled()方法的使用

作为设置方法,调用的地方有好多个,总结如下:

  • AsyncTaskMethodReturnValueHandler:处理返回值类型是WebAsyncTask的方法
// 若返回null,就没必要继续处理了
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
  • CallableMethodReturnValueHandler/DeferredResultMethodReturnValueHandler/StreamingResponseBodyReturnValueHandler:处理返回值类型是Callable/DeferredResult/ListenableFuture/CompletionStage/StreamingResponseBody的方法(原理同上)
  • HttpEntityMethodProcessor:返回值类型是HttpEntity的方法
// 看一看到,这种返回值的都会标注为已处理,这样就不再需要视图(渲染)了
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true); // 第一句就是这句代码
if (returnValue == null) {
return;
}
... // 交给消息处理器去写
outputMessage.flush();
}
  • 同上的原理的还有HttpHeadersReturnValueHandler/RequestResponseBodyMethodProcessor/ResponseBodyEmitterReturnValueHandler等等返回值处理器
  • ServletInvocableHandlerMethod/HandlerMethod在处理Handler方法时,有时也会标注true已处理(比如:get请求NotModified/已设置了HttpStatus状态码/isRequestHandled()==true等等case)。除了这些case,method方法执行完成后可都会显示设置false的(因为执行完handlerMethod后,还需要交给视图渲染~)
  • ServletResponseMethodArgumentResolver:这唯一一个是处理入参时候的。若入参类型是ServletResponse/OutputStream/Writer,并且mavContainer != null,它就设置为true了(因为Spring MVC认为既然你自己引入了response,那你就自己做输出吧,因此使用时此处是需要特别注意的细节地方~)
resolveArgument()方法:

		if (mavContainer != null) {
mavContainer.setRequestHandled(true); // 相当于说你自己需要`ServletResponse`,那返回值就交给你自己处理吧~~~~
}

本文最重要类:ModelAndViewContainer部分就介绍到这。接下来就介绍就很简单了,轻松且愉快


Model

org.springframework.ui.Model的概念不管是在MVC设计模式上,还是在Spring MVC里都是被经常提到的:它用于控制层给前端返回所需的数据(渲染所需的数据)

//  @since 2.5.1 它是一个接口
public interface Model {
...
// addAttribute/addAllAttributes/mergeAttributes/containsAttribute
...
// Return the current set of model attributes as a Map.
Map<String, Object> asMap();
}

它的继承树如下:



最重要的那必须是ExtendedModelMap啊,它留到介绍ModelMap的时候再详说,简单看看其余子类。

RedirectAttributes

从命名就能看出是和重定向有关的,它扩展了Model接口:

// @since 3.1
public interface RedirectAttributes extends Model {
...
// 它扩展的三个方法,均和flash属性有关
RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue);
// 这里没指定key,因为key根据Conventions#getVariableName()自动生成
RedirectAttributes addFlashAttribute(Object attributeValue);
// Return the attributes candidate for flash storage or an empty Map.
Map<String, ?> getFlashAttributes();
}
RedirectAttributesModelMap

它实现了RedirectAttributes接口,同时也继承自ModelMap,所以"间接"实现了Model接口的所有方法。

public class RedirectAttributesModelMap extends ModelMap implements RedirectAttributes {
@Nullable
private final DataBinder dataBinder;
private final ModelMap flashAttributes = new ModelMap();
...
@Override
public RedirectAttributesModelMap addAttribute(String attributeName, @Nullable Object attributeValue) {
super.addAttribute(attributeName, formatValue(attributeValue));
return this;
} // 可见这里的dataBinder是用于数据转换的
// 把所有参数都转换为String类型(因为Http都是string传参嘛)
@Nullable
private String formatValue(@Nullable Object value) {
if (value == null) {
return null;
}
return (this.dataBinder != null ? this.dataBinder.convertIfNecessary(value, String.class) : value.toString());
}
... @Override
public Map<String, Object> asMap() {
return this;
}
@Override
public RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue) {
this.flashAttributes.addAttribute(attributeName, attributeValue);
return this;
}
...
}

我认为它唯一自己的做的有意义的事:借助DataBinder把添加进来的属性参数会转为String类型(为何是转换为String类型,你有想过吗???)~

ConcurrentModel

它是Spring5.0后才有的,是线程安全的Model,并没提供什么新鲜东西,略(运用于有线程安全问题的场景)


ModelMap

ModelMap继承自LinkedHashMap,因此它的本质其实就是个Map而已。

它的特点是:借助Map的能力间接的实现了org.springframework.ui.Model的接口方法,这种设计技巧更值得我们参考学习的(曲线救国的意思有木有~)。



so,这里只需要看看ExtendedModelMap即可。它自己继承自ModelMap,没有啥特点,全部是调用父类的方法完成的接口方法复写,喵喵他的子类吧~

BindingAwareModelMap

注意:它和普通ModelMap的区别是:它能感知数据校验结果(如果放进来的key存在对应的绑定结果,并且你的value不是绑定结果本身。那就移除掉MODEL_KEY_PREFIX + key这个key的键值对~)。

public class BindingAwareModelMap extends ExtendedModelMap {

	// 注解复写了Map的put方法,一下子就拦截了所有的addAttr方法。。。
@Override
public Object put(String key, Object value) {
removeBindingResultIfNecessary(key, value);
return super.put(key, value);
}
@Override
public void putAll(Map<? extends String, ?> map) {
map.forEach(this::removeBindingResultIfNecessary);
super.putAll(map);
} // 本类处理的逻辑:
private void removeBindingResultIfNecessary(Object key, Object value) {
// key必须是String类型才会给与处理
if (key instanceof String) {
String attributeName = (String) key;
if (!attributeName.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attributeName;
BindingResult bindingResult = (BindingResult) get(bindingResultKey); // 如果有校验结果,并且放进来的value值不是绑定结果本身,那就移除掉绑定结果(相当于覆盖掉)
if (bindingResult != null && bindingResult.getTarget() != value) {
remove(bindingResultKey);
}
}
}
}
}

Spring MVC默认使用的就是这个ModelMap,但它提供的感知功能大多数情况下我们都用不着。不过反正也不用你管,乖乖用着呗


ModelAndView

顾名思义,ModelAndView指模型和视图的集合,既包含模型又包含视图;ModelAndView一般可以作为Controller的返回值,所以它的实例是开发者自己手动创建的,这也是它和上面的主要区别(上面都是容器创建,然后注入给我们使用的~)。

因为这个类是直接面向开发者的,所以建议里面的一些API还是要熟悉点较好:

public class ModelAndView {
@Nullable
private Object view; // 可以是View,也可以是String
@Nullable
private ModelMap model; // 显然,你也可以自己就放置好一个http状态码进去
@Nullable
private HttpStatus status;
// 标记这个实例是否被调用过clear()方法~~~
private boolean cleared = false; // 总共这几个属性:它提供的构造函数非常的多 这里我就不一一列出
public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
public void setView(@Nullable View view) {
this.view = view;
}
@Nullable
public String getViewName() {
return (this.view instanceof String ? (String) this.view : null);
}
@Nullable
public View getView() {
return (this.view instanceof View ? (View) this.view : null);
}
public boolean hasView() {
return (this.view != null);
}
public boolean isReference() {
return (this.view instanceof String);
} // protected方法~~~
@Nullable
protected Map<String, Object> getModelInternal() {
return this.model;
}
public ModelMap getModelMap() {
if (this.model == null) {
this.model = new ModelMap();
}
return this.model;
} // 操作ModelMap的一些方法如下:
// addObject/addAllObjects public void clear() {
this.view = null;
this.model = null;
this.cleared = true;
}
// 前提是:this.view == null
public boolean isEmpty() {
return (this.view == null && CollectionUtils.isEmpty(this.model));
} // 竟然用的was,歪果仁果然严谨 哈哈
public boolean wasCleared() {
return (this.cleared && isEmpty());
}
}

很多人疑问:为何Controller的处理方法不仅仅可以返回ModelAndView,还可以通过返回Map/Model/ModelMap等来直接向页面传值呢???如果返回值是后三者,又是如何找到view完成渲染的呢?

这个问题我抛出来,本文不给答案。因为都聊到这了,此问题应该不算难的了,建议小伙伴必须自行弄懂缘由(请不要放过有用的知识点)。若实在有不懂之处可以给留言我会帮你解答的~

答案参考提示:可参阅ModelMethodProcessorModelMethodProcessor对返回值的处理模块

绝大多数情况下,我都建议返回ModelAndView,而不是其它那哥三。因为它哥三都没有指定视图名,所以通过DispatcherServlet.applyDefaultViewName()生成的视图名一般都不是我们需要的。(除非你的目录、命名等等都特别特别的规范,那顺便倒是可以省不少事~~~


ModelFactory

关于ModelFactory它的介绍,这篇文章 里算是已经详细讲解过了,这里再简述两句它的作用。

ModelFactory是用来维护Model的,具体包含两个功能

  1. 初始化Model
  2. 处理器执行后将Model中相应的参数更新到SessionAttributes中(处理@ModelAttribute@SessionAttributes

总结

本以为本文不会很长的,没想到还是写成了超10000字的中篇文章。希望这篇文章能够帮助你对Spring MVC对模型、视图这块核心内容的理解,帮你扫除途中的一些障碍,共勉~

若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞

ModelAndViewContainer、ModelMap、Model详细介绍【享学Spring MVC】的更多相关文章

  1. HandlerMethodArgumentResolver(三):基于消息转换器的参数处理器【享学Spring MVC】

    每篇一句 一个事实是:对于大多数技术,了解只需要一天,简单搞起来只需要一周.入门可能只需要一个月 前言 通过 前面两篇文章 的介绍,相信你对HandlerMethodArgumentResolver了 ...

  2. HandlerMethodArgumentResolver(二):Map参数类型和固定参数类型【享学Spring MVC】

    每篇一句 黄金的导电性最好,为什么电脑主板还是要用铜? 飞机最快,为什么还有人做火车? 清华大学最好,为什么还有人去普通学校? 因为资源都是有限的,我们现实生活中必须兼顾成本与产出的平衡 前言 上文 ...

  3. 内容协商在视图View上的应用【享学Spring MVC】

    每篇一句 人生很有意思:首先就得活得长.活得长才能够见自己,再长就可以见众生 前言 在经过 前两篇 文章了解了Spring MVC的内容协商机制之后,相信你已经能够熟练的运用Spring MVC提供的 ...

  4. HandlerMethodArgumentResolver(一):Controller方法入参自动封装器【享学Spring MVC】

    每篇一句 你的工作效率高,老板会认为你强度不够.你代码bug多,各种生产环境救火,老板会觉得你是团队的核心成员. 前言 在享受Spring MVC带给你便捷的时候,你是否曾经这样疑问过:Control ...

  5. Spring MVC内置支持的4种内容协商方式【享学Spring MVC】

    每篇一句 十个光头九个富,最后一个会砍树 前言 不知你在使用Spring Boot时是否对这样一个现象"诧异"过:同一个接口(同一个URL)在接口报错情况下,若你用rest访问,它 ...

  6. Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】

    每篇一句 在绝对力量面前,一切技巧都是浮云 前言 上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍.本文主要针对Spring MVC内容协商方式:从步骤.原理 ...

  7. RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】

    每篇一句 做事的人和做梦的人最大的区别就是行动力 前言 本文为深入了解Spring提供的Rest调用客户端RestTemplate开山,对它相关的一些组件做讲解. Tips:请注意区分RestTemp ...

  8. RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】

    每篇一句 人圆月圆心圆,人和家和国和---中秋节快乐 前言 在阅读本篇之前,建议先阅读开山篇效果更佳.RestTemplate是Spring提供的用于访问Rest服务的客户端工具,它提供了多种便捷访问 ...

  9. 从原理层面掌握@InitBinder的使用【享学Spring MVC】

    每篇一句 大魔王张怡宁:女儿,这堆金牌你拿去玩吧,但我的银牌不能给你玩.你要想玩银牌就去找你王浩叔叔吧,他那银牌多 前言 为了讲述好Spring MVC最为复杂的数据绑定这块,我前面可谓是做足了功课, ...

随机推荐

  1. java性能优化--字符串优化处理

    String对象 String对象是java中重要的数据类型,在大部分情况下我们都会用到String对象.其实在Java语言中,其设计者也对String做了大量的优化工作,这些也是String对象的特 ...

  2. 【MySQL】目录、文件权限问题

    详情如下: $ cat /usr/local/mysql/data/Phoenix-slow.log cat: /usr/local/mysql/data/Phoenix-slow.log: Perm ...

  3. 使用f12定位bug

    为什么找到网站中的bug后还要去分析它到底是属于前端bug还是后端bug 三个原因: 1.在一些公司,一个系统可能是由前端团队和后端团队共同开发出来的,因此在分配bug的时候,不同模块的bug一般都会 ...

  4. 设置Myeclipse的jvm内存参数

    Myeclipse经常会遇到内存溢出和Gc开销过大的情况,这时候就需要修改Myeclipse的Jvm内存参数 修改如下:(使用Extjs做公司大项目时候,不要让项目Builders的Javascrip ...

  5. SpringBoot入门及YML文件详解

    SpringBoot 简介 微框架,与 Spring4 一起诞生,基于约定.生来为了简化 spring 的配置 优点 可以快速的上手,整合了一些子项目(开源框架或者第三方开源库) 可以依赖很少的配置快 ...

  6. Unity实现放大缩小以及相机位置平移实现拖拽效果

    放大缩小功能是游戏开发中用到的功能,今天就来讲一下Unity中放大缩小怎么实现. 1.IDragHandler, IBeginDragHandler, IEndDragHandler这三个接口是Uni ...

  7. Android Bluetooth Low Energy (BLE)简单方便的蓝牙开源库——EasyBLE

    源码传送门 最新版本 功能 支持多设备同时连接 支持广播包解析 支持连接同时配对 支持搜索系统已连接设备 支持搜索器设置 支持自定义搜索过滤条件 支持自动重连.最大重连次数限制.直接重连或搜索到设备再 ...

  8. Asp.Net Core WebAPI+PostgreSQL部署在Docker中

     PostgreSQL是一个功能强大的开源数据库系统.它支持了大多数的SQL:2008标准的数据类型,包括整型.数值值.布尔型.字节型.字符型.日期型.时间间隔型和时间型,它也支持存储二进制的大对像, ...

  9. Hive 系列(一)—— Hive 简介及核心概念

    一.简介 Hive 是一个构建在 Hadoop 之上的数据仓库,它可以将结构化的数据文件映射成表,并提供类 SQL 查询功能,用于查询的 SQL 语句会被转化为 MapReduce 作业,然后提交到 ...

  10. Kali Linux无法访问网络的问题

    首先 ping www.baidu.com ping: unkown host www.baidu.com 然后 ping 8.8.8.8 connect:network is unreachable ...