Spring學習日記十四------MVC 視圖呈現篇

DispatcherServlet 視圖設計

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

DispatcherServlet.java 

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 對象,就行分析。 

        由此,我們可以看出,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 中實現的。下面是他的主要時序圖:

AbstractView.java 

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()方法。

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.java

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.java

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 頁面展現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章