• 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 的核心内容。其他的问题,都是建立在该问题的基础上,或者是对这几个问题的一种延伸。

    还有想说的是,在边测试边看源码边写文章的时候,给我这么一个感觉,不论是何种开源框架,它所有的应用都是从源码中来的,不论哪个人或者那本书把这个知识讲的多好,

    但是在我看来,想要学好某个框架,都要到它源码中去,找到它的根,这样才会有理有据,不会忘的那么快,纵然忘了,下次还是可以找出来为什么。

    纵然在看源码的过程中,可能会遇到很多困难,比如英文单词不认识,整个句子读不通,但是如果你能结合自己的理解,

    然后理解文档中的话,我相信对你帮助是很大的,其实这就是一种自学能力,摸的着,看得见。

    算是给各位看客老爷的一种建议吧,愿各位在学习的道路上不要停滞不前。

  • 相关阅读:
    JDBC
    uml 和 unified process
    关于N9手机第三种交互方式的思考和再设计
    [jQuery插件] jQuery Color Animations颜色动画插件
    Azul发布开源工具jHiccup,为Java提供运行时响应时间分析
    行内元素verticalalign:middle在html5和xhtml1.0及以下版本中的表现差异
    使用maven进行scala项目的构建
    计划FM为人人网提供首个开源Ruby SDK
    Chrome扩展:Run Selected HTML
    Team Foundation Service更新:改善了导航和项目状态速查功能
  • 原文地址:https://www.cnblogs.com/solverpeng/p/5743609.html
Copyright © 2020-2023  润新知