一、本來想說的是返回值處理問題,但在 SpringMVC 中,返回值處理問題的核心就是視圖渲染。所以這裡標題叫視圖渲染問題。
本來想在上一篇文章中對視圖解析進行說明的,但是通過源碼發現,它應該算到視圖渲染中,所以在這篇文章中進行說明。
org.springframework.web.servlet.DispatcherServlet#doDispatch方法中
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//945行返回了 ModelAndView 對象
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);// 959行進行的就是返回值處理問題
org.springframework.web.servlet.DispatcherServlet#processDispatchResult方法中
render(mv, request, response); //1012進行視圖的渲染(包含視圖解析)
org.springframework.web.servlet.DispatcherServlet#render 方法
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. Locale locale = this.localeResolver.resolveLocale(request); response.setLocale(locale); View view; if (mv.isReference()) { // We need to resolve the view name. view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException( "Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name '" + getServletName() + "'"); } } // Delegate to the View object for rendering. if (logger.isDebugEnabled()) { logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); } try { view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'", ex); } throw ex; } }
可以看到有兩個 if ,第一個 if 解決的是視圖解析問題,第二個 if 解決的是視圖渲染問題。還有官方是這樣描述這個方法的:Render the given ModelAndView.
二、視圖解析:通過視圖解析器進行視圖的解析
1.解析一個視圖名到一個視圖對象,具體解析的過程是:在容器中查找所有配置好的視圖解析器(List類型),然後進行遍歷,
只要有一個視圖解析器能解析出視圖就返回 View 對象,若遍歷完成後都不能解析出視圖,那麼返回 null。
具體來看:
org.springframework.web.servlet.DispatcherServlet#resolveViewName
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } return null; }
2. ViewResolver
(1)官方描述:
* Interface to be implemented by objects that can resolve views by name.
*
* <p>View state doesn't change during the running of the application,
* so implementations are free to cache views.
*
* <p>Implementations are encouraged to support internationalization,
* i.e. localized view resolution.
說明:
ViewResolver 接口由能解析視圖名稱的實現類來實現。
在程序運行期間視圖的狀態不能更改,所以實現能被隨意緩存。鼓勵實現支持國際化。
(2)ViewResolver 的整個體系
可以看出 SpringMVC 提供了很多類型視圖解析器。
(3)在 SpringMVC 的第一篇文章中,配置過一個視圖解析器。
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>
發送一個請求之後,發現要遍歷的 ViewResolvers 只有一個,就是上面的這個 ViewResolver。沒有其他默認的視圖解析器。所以說在SpringMVC 配置文件中,必須配置至少一個 視圖解析器。
那麼這裡會有一個問題?如果配置多個視圖解析器,他們的遍歷順序是怎麼樣的呢?
ViewResolver 的所有實現類中都存在一個 order 屬性。
看這個屬性的 setOrder() 注釋:Set the order in which this {@link org.springframework.web.servlet.ViewResolver} is evaluated。設置誰先被評估。
還有一點小不同:
除 ContentNegotiatingViewResolver 之外,其他所有的 ViewResolver 的默認值都是:Integer.MAX_VALUE(2^31 -1,即2147483647),
而 ContentNegotiatingViewResolver 的默認值為 Ordered.HIGHEST_PRECEDENCE(-2147483648)。
那他們的遍歷的順序與 order 是什麼關系呢?如果不設置 order 的話,遍歷順序又是怎麼樣的?
<1>設置 order 屬性後,遍歷順序是怎麼樣的
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> <property name="order" value="99"/> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> <property name="order" value="-99"/> </bean>
對 BeanNameViewResolver 的 order 屬性指定為 99,對 InternalResourceViewResolver 指定為-99。這裡故意將 BeanNameViewResolver 放到了 InternalResourceViewResolver 前面。
遍歷順序:
發現 InternalResourceViewResolver 會先被遍歷。
結論:
在指定 order 屬性的情況下,order 值越小的,越先會遍歷。
<2>不設置 order 屬性,遍歷順序是怎樣的
在測試這個的時候,發現一個這樣的現象:我將 BeanNameViewResolver 和 InternalResourceViewResolver 的 order 屬性都去掉,我這裡用的是 Jrebel 的熱部署。發現再次請求的時候,
這兩個視圖的 order 屬性值還和之前的一樣:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> </bean>
為什麼呢?想起了官方的描述:"在程序運行期間視圖的狀態不能更改,所以實現能被隨意緩存",這裡被緩存了。重啟後來看真正的測試。
第一種情況:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> </bean>
第二種情況:將 bean 在 SpringMVC Config 文件中的順序進行替換,需要注意的是,重啟服務器,否則它們的順序還是會被緩存下來。重啟後來看:
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver"> </bean> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>
結論已經很明顯了:
在同等優先級的情況下,遍歷的順序是由 ViewResolver 在 SpringMVC Config 文件中配置的順序決定的,誰在前誰先遍歷。
這裡不對具體的每個視圖解析器進行說明,路已經指明了。
3.ViewResolver 具體是怎麼將 view name 解析為一個視圖的?
先看 ViewResolver 中的 View resolveViewName(String viewName, Locale locale)
說明:
解析視圖通過其名稱。注意:允許 ViewResolver 鏈。
如果一個給定名稱的view 沒有在一個 ViewResolver 中定義,那麼它應該返回 null。
然而它也不是必須的:有一些 ViewResolver 當嘗試通過視圖名稱構建 View 對象失敗後,不返回 null。而替代它的是,拋出一個異常。
以 org.springframework.web.servlet.view.AbstractCachingViewResolver 進行分析。
@Override public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { return createView(viewName, locale); } else { Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); if (logger.isTraceEnabled()) { logger.trace("Cached view [" + cacheKey + "]"); } } } } } return (view != UNRESOLVED_VIEW ? view : null); } }
判斷該視圖是否被緩存,如果沒有被緩存,則創建視圖,如果被緩存,則從緩存中獲取。
創建視圖,以 InternalResourceViewResolver 和 BeanNameViewResolver 為例:
(1)InternalResourceViewResolver
org.springframework.web.servlet.view.UrlBasedViewResolver#createView
protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); return applyLifecycleMethods(viewName, view); } // Check for special "forward:" prefix. if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); return new InternalResourceView(forwardUrl); } // Else fall back to superclass implementation: calling loadView. return super.createView(viewName, locale); }
在創建視圖前會檢查返回值是否是以:"redirect:" 或 "forward:" 開頭的。
如果是重定向:則創建一個重定向視圖,返回創建的視圖。如果是轉發:則返回通過 轉發 url 創建的 InternalResourceView 視圖。
org.springframework.web.servlet.view.UrlBasedViewResolver#loadView
AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null);
調用具體的 InternalResourceViewResolver ,然後又調用 父類的 buildView() 方法
org.springframework.web.servlet.view.UrlBasedViewResolver#buildView
protected AbstractUrlBasedView buildView(String viewName) throws Exception { AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass()); view.setUrl(getPrefix() + viewName + getSuffix()); String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); if (this.exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } return view; }
可以看出:是通過 BeanUtils.instantiateClass(getViewClass()) 來創建 View 對象的。這個例子與其說是 InternalResourceViewResolver ,倒不如說是 UrlBasedViewResolver 類型的例子。
從這裡也可以看出:該類型最終要到的目標URL為:getPrefix() + viewName + getSuffix()
(2)BeanNameViewResolver
public View resolveViewName(String viewName, Locale locale) throws BeansException { ApplicationContext context = getApplicationContext(); if (!context.containsBean(viewName)) { // Allow for ViewResolver chaining. return null; } return context.getBean(viewName, View.class); }
org.springframework.context.support.AbstractApplicationContext#getBean(java.lang.String, java.lang.Class<T>)
public <T> T getBean(String name, Class<T> requiredType) throws BeansException { this.assertBeanFactoryActive(); return this.getBeanFactory().getBean(name, requiredType); }
可以看出:是通過 BeanFactory.getBean(String name, Class<T> requiredType) 來獲取的。
三、視圖渲染
1.View
官方文檔:
* MVC View for a web interaction. Implementations are responsible for rendering
* content, and exposing the model. A single view exposes multiple model attributes.
*
* <p>This class and the MVC approach associated with it is discussed in Chapter 12 of
* <a href="http://www.amazon.com/exec/obidos/tg/detail/-/0764543857/">Expert One-On-One J2EE Design and Development</a>
* by Rod Johnson (Wrox, 2002).
*
* <p>View implementations may differ widely. An obvious implementation would be
* JSP-based. Other implementations might be XSLT-based, or use an HTML generation library.
* This interface is designed to avoid restricting the range of possible implementations.
*
* <p>Views should be beans. They are likely to be instantiated as beans by a ViewResolver.
* As this interface is stateless, view implementations should be thread-safe.
說明:
SpringMVC 對一個 web 來說是相互作用的(不太明白)。View 的實現類是負責呈現內容的,並且 exposes(暴露、揭露、揭發的意思,這裡就按暴露解釋吧,想不出合適的詞語) 模型的。
一個單一的視圖可以包含多個模型。
View 的實現可能有很大的不同。一個明顯的實現是基於 JSP 的。其他的實現可能是基於 XSLT 的,或者是一個 HTML 生成庫。
設計這個接口是為了避免約束可能實現的范圍(這裡是不是說,我們可以通過實現該接口來自定義擴展自定義視圖?)。
所有的視圖都應該是一個 Bean 類。他們可能被 ViewResolver 當做一個 bean 進行實例化。
由於這個接口是無狀態的,View 的所有實現類應該是線程安全的。
2.View 的整個體系
3.具體渲染的一個過程
org.springframework.web.servlet.view.AbstractView#render
說明一下這個方法:
為指定的模型指定視圖,如果有必要的話,合並它靜態的屬性和RequestContext中的屬性,renderMergedOutputModel() 執行實際的渲染。
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isTraceEnabled()) { logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + " and static attributes " + this.staticAttributes); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); renderMergedOutputModel(mergedModel, request, response); }
這裡只看 org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel 這個方法
@Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine which request handle to expose to the RequestDispatcher. HttpServletRequest requestToExpose = getRequestToExpose(request); // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, requestToExpose); // Expose helpers as request attributes, if any. exposeHelpers(requestToExpose); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(requestToExpose, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(requestToExpose, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.include(requestToExpose, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.forward(requestToExpose, response); } }
可以看到前面的幾個步驟都是為 RequestDispatch 做准備,裝填數據。最後,到目標頁面是通過轉發。
四、總結
介紹了 SpringMVC 視圖解析和視圖渲染問題是如何解決的。SpringMVC 為邏輯視圖提供了多種視圖解析策略,可以在配置文件中配置多個視圖的解析策略。並制定其先後順序。
這裡所說的視圖解析策略,就是指視圖解析器。視圖解析器會將邏輯視圖名解析為一個具體的視圖對象。再說視圖渲染的過程,視圖對模型進行了渲染,最終將模型的數據以某種形式呈現給用戶。
到此為止,在我看來,SpringMVC 整個流程已經跑完,前面的幾篇文章,從一個小栗子開始,然後分別介紹了請求映射問題,參數問題,返回值問題,到這篇文章,返回值處理問題。
我認為這幾個問題是整個 SpringMVC 的核心內容。其他的問題,都是建立在該問題的基礎上,或者是對這幾個問題的一種延伸。
還有想說的是,在邊測試邊看源碼邊寫文章的時候,給我這麼一個感覺,不論是何種開源框架,它所有的應用都是從源碼中來的,不論哪個人或者那本書把這個知識講的多好,
但是在我看來,想要學好某個框架,都要到它源碼中去,找到它的根,這樣才會有理有據,不會忘的那麼快,縱然忘了,下次還是可以找出來為什麼。
縱然在看源碼的過程中,可能會遇到很多困難,比如英文單詞不認識,整個句子讀不通,但是如果你能結合自己的理解,
然後理解文檔中的話,我相信對你幫助是很大的,其實這就是一種自學能力,摸的著,看得見。
算是給各位看客老爺的一種建議吧,願各位在學習的道路上不要停滯不前。