ModelAndViewContainer、ModelMap、Model詳細介紹【享學Spring MVC】


每篇一句

一個開源的技術產品做得好不好,主要是看你能解決多少非功能性問題(因為功能性問題是所有產品都能夠想到的)

前言

寫這篇文章非我本意,因為我覺得對如題的這個幾個類的了解還是比較基礎且簡單的一塊內容,直到有超過兩個同學問過我一些問題的時候:通過聊天發現小伙伴都聽說過這幾個類,但對於他們的使用、功能定位是傻傻分不清楚的(因為名字上都有很多的相似之處)。
那么書寫本文就是當作一篇科普類文章記錄下來,已經非常熟悉小伙伴就沒太大必要往下繼續閱讀本文內容了,因為這塊不算難的(當然我只是建議而已~)。

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,手動邀請你入群一起飛


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM