SpringMVC核心——視圖渲染(包含視圖解析)問題

来源:http://www.cnblogs.com/solverpeng/archive/2016/08/08/5743609.html
-Advertisement-
Play Games

一、本來想說的是返回值處理問題,但在 SpringMVC 中,返回值處理問題的核心就是視圖渲染。所以這裡標題叫視圖渲染問題。 本來想在上一篇文章中對視圖解析進行說明的,但是通過源碼發現,它應該算到視圖渲染中,所以在這篇文章中進行說明。 org.springframework.web.servlet. ...


一、本來想說的是返回值處理問題,但在 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 的核心內容。其他的問題,都是建立在該問題的基礎上,或者是對這幾個問題的一種延伸。

 

還有想說的是,在邊測試邊看源碼邊寫文章的時候,給我這麼一個感覺,不論是何種開源框架,它所有的應用都是從源碼中來的,不論哪個人或者那本書把這個知識講的多好,

但是在我看來,想要學好某個框架,都要到它源碼中去,找到它的根,這樣才會有理有據,不會忘的那麼快,縱然忘了,下次還是可以找出來為什麼。

縱然在看源碼的過程中,可能會遇到很多困難,比如英文單詞不認識,整個句子讀不通,但是如果你能結合自己的理解,

然後理解文檔中的話,我相信對你幫助是很大的,其實這就是一種自學能力,摸的著,看得見。

 

算是給各位看客老爺的一種建議吧,願各位在學習的道路上不要停滯不前。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 先上一段最近項目中的代碼,此代碼可以放到自己項目中的dbContext中 EF6中可以覆寫SaveChangesAsync(非同步)或者SaveChanges來實現記錄變化的跟蹤,這其中包括新增、修改、和刪除,dbContext中的屬性ChangeTracker可以跟蹤屬性的變化,即查找實體修改記錄: ...
  • 按照網上的教程寫了一個cURL的小例子,在apache環境下執行,一點反應也沒有,放在IIS環境里就ok的,感覺問題一定出在動態連接庫上,因為配置文件里的php_curl.dll已經打開了,而且在iis上ok; 網上找了一些解決方案: 設置了【環境變數】:phpext,PHPRC;無效 把php_c ...
  • 博客園上傳代碼時拷貝vs裡面的代碼不能直接粘貼,否則空格會不符合要求 去掉空格代碼 ...
  • 實例運行結果如下 實例運行結果如下 實例運行結果如下 ...
  • 詳細的見 https://github.com/linux-wang/DRF_tutorial/blob/master/README.md DRF中有一個serializer的概念,實現的功能是將各種Django Queryset和model instance轉換成Python原生格式,這裡就省去了 ...
  • 實力運行效果如下 ...
  • String類和StringBuffer類主要用來處理字元串,這兩個類提供了很多字元串的使用處理方法。String類是不可變類,表示對象所包含的字元串類不能改變。StringBuffer類是可變類,其對象所包含的字元串內容可以被添加或修改。 關於這兩個類處理字元串的常用方法請參考:http://ww ...
  • SpEL簡介與功能特性 Spring表達式語言(簡稱SpEL)是一個支持查詢併在運行時操縱一個對象圖的功能強大的表達式語言。SpEL語言的語法類似於統一EL,但提供了更多的功能,最主要的是顯式方法調用和基本字元串模板函數。 同很多可用的Java 表達式語言相比,例如OGNL,MVEL和JBoss E ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...