程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> SpringMVC核心——視圖渲染(包含視圖解析)問題,springmvc視圖

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

編輯:JAVA綜合教程

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


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

 

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

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

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

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

 

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

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved