Spring 源碼分析(四) ——MVC(七)視圖呈現

DispatcherServlet 視圖設計

        前面分析了 Spring MVC 中的 M(Model)和 C(Controller)相關的實現,其中的 M 大體對應 ModelAndView 的生成,而 C 大體對應 DispatcherServlet 和與用戶業務邏輯相關的 handler 實現。在 Spring MVC 框架中,DispatcherServlet 起到了很是核心的做用,是整個 MVC 框架的調度樞紐。對應視圖呈現功能,它的調用入口一樣在 DispatcherServlet 中的 doDispatch 方法中實現。具體來講,它的調用入口是 DispatcherServlet 中的 render 方法。java

DispatcherServlet.java web

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
   // 從 request 中讀取 locale 信息,並設置 response 的 locale 值
   Locale locale = this.localeResolver.resolveLocale(request);
   response.setLocale(locale);

   View view;
   // 根據 ModleAndView 中設置的視圖名稱進行解析,獲得對應的視圖對象
   if (mv.isReference()) {
      // 須要對象視圖名進行解析
      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() + "'");
      }
   }
   // ModelAndView 中有可能已經直接包括了 View 對象,那就能夠直接使用
   else {
      // 直接從 ModelAndView 對象中取得實際的視圖對象
      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() + "'");
      }
   }

   // 提交視圖對象進行展示
   if (logger.isDebugEnabled()) {
      logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
   }
   try {
      // 調用 View 實現對數據進行呈現,並經過 HTTPResponse 把視圖呈現給 HTTP 客戶端
      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;
   }
}

protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
      HttpServletRequest request) throws Exception {

   // 調用 ViewResolver 進行解析
   for (ViewResolver viewResolver : this.viewResolvers) {
      View view = viewResolver.resolveViewName(viewName, locale);
      if (view != null) {
         return view;
      }
   }
   return null;
}


View 接口的設計

        下面須要對獲得的 View 對象,就行分析。 
app

        由此,咱們能夠看出,Spring MVC 對 經常使用的視圖提供的支持。從這個體系中咱們能夠看出,Spring MVC 對經常使用視圖的支持,好比 JSP/JSTL 視圖、FreeMaker 視圖等等。View 的設計實際上是很是簡單的,只須要實現 Render 接口。
框架

public interface View {
   String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
   String PATH_VARIABLES = View.class.getName() + ".pathVariables";
   String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
   String getContentType();
   void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
}


JSP 視圖的實現

        使用 JSP 的頁面做爲 Web UI,是使用 Java 設計 Web 應用比較常見的選擇之一,若是在 JSP 中使用 Jstl(JSP Standard Tag Library)來豐富 JSP 的功能,在 Spring MVC 中就須要使用 JstlView 來做爲 View 對象,從而對數據進行視圖呈現。而 JstlView 沒有實現 render 的方法,而使用的 render 方法是它的基類 AbstractView 中實現的。下面是他的主要時序圖:
oop

AbstractView.java this

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 裏
   Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
   prepareResponse(request, response);
   // 展示模型數據到視圖的調用方法
   renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

        這個基類的 render 方法實現並不困難,而它主要是完成數據的準備工做,好比把全部的數據模型進行整合放到 mergedModel 對象中,而它是一個 HasMap。而後,調用 renderMergedOutputModel()方法。
spa

protected void renderMergedOutputModel(
      Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

   // 判斷須要將哪個請求的處理器交給 RequestDispatcher
   exposeModelAsRequestAttributes(model, request);

   // 對數據進行處理
   exposeHelpers(request);

   // Determine the path for the request dispatcher.
   String dispatcherPath = prepareForRendering(request, response);

   // Obtain a RequestDispatcher for the target resource (typically a JSP).
   RequestDispatcher rd = getRequestDispatcher(request, 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(request, response)) {
      response.setContentType(getContentType());
      if (logger.isDebugEnabled()) {
         logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
      }
      rd.include(request, response);
   }

   else {
      // Note: The forwarded resource is supposed to determine the content type itself.
      // 轉發請求到內部定義好的資源上,好比 JSP 頁面,JSP 頁面的展示由 Web 容器完成,
      // 在這種狀況下,View 只是起到轉發請求的做用
      if (logger.isDebugEnabled()) {
         logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
      }
      rd.forward(request, response);
   }
}

AbstractView.javadebug

protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception {
   for (Map.Entry<String, Object> entry : model.entrySet()) {
      String modelName = entry.getKey();
      Object modelValue = entry.getValue();
      if (modelValue != null) {
         request.setAttribute(modelName, modelValue);
         if (logger.isDebugEnabled()) {
            logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() +
                  "] to request in view with name '" + getBeanName() + "'");
         }
      }
      else {
         request.removeAttribute(modelName);
         if (logger.isDebugEnabled()) {
            logger.debug("Removed model object '" + modelName +
                  "' from request in view with name '" + getBeanName() + "'");
         }
      }
   }
}

JstlView.java設計

protected void exposeHelpers(HttpServletRequest request) throws Exception {
   if (this.messageSource != null) {
      JstlUtils.exposeLocalizationContext(request, this.messageSource);
   }
   else {
      JstlUtils.exposeLocalizationContext(new RequestContext(request, getServletContext()));
   }
}

InternalResourceView.javacode

protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response)
      throws Exception {

   // 從 request 中獲取 URL 路徑
   String path = getUrl();
   if (this.preventDispatchLoop) {
      String uri = request.getRequestURI();
      if (path.startsWith("/") ? uri.equals(path) : uri.equals(StringUtils.applyRelativePath(uri, path))) {
         throw new ServletException("Circular view path [" + path + "]: would dispatch back " +
               "to the current handler URL [" + uri + "] again. Check your ViewResolver setup! " +
               "(Hint: This may be the result of an unspecified view, due to default view name generation.)");
      }
   }
   return path;
}

        獲得 URL 路徑後,使用 RequestDispatcher 把請求轉發到這個資源上,就完成了 JSTL 的 JSP 頁面展示。



——水門(2016年4月寫於杭州)

相關文章
相關標籤/搜索