菜鸟之路——Spring MVC(五)ViewResolver

    在Spring MVC中,ViewResolver 用来将用户控制器生成的返回结果解析成视图,View定义了不同的视图,并渲染给用户,但是Spring是怎样工作的呢?现在我们就分析下Spring如何解析返回的结果生成响应的视图。

  一、概念

  View
View接口表示一个响应给用户的视图,例如jsp文件,pdf文件,html文件等,它的定义如下

<span style="font-size:14px;">public interface View {
    //HttpServletRequest中的属性名,其值为响应状态码
    String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";
    //HttpServletRequest中的属性名,它的对应值是请求路径中的变量,及@PathVariable
    //注解的变量
    String PATH_VARIABLES = View.class.getName() + ".pathVariables";
    //该视图的ContentType
    String getContentType();
    //渲染该视图
    void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response);
}</span>

      
该接口只有两个方法定义,分别表明该视图的ContentType和如何被渲染。Spring中提供了丰富的视图支持,几乎包含所有你想得到的,并且Spring的视图拓展性很好,你可以轻松实现自己的视图。下面是View的一些实现类:
                  《菜鸟之路——Spring MVC(五)ViewResolver》

   
类结构图:
《菜鸟之路——Spring MVC(五)ViewResolver》



   ViewResolver
ViewResolver接口定义了如何通过view 名称来解析对应View实例的行为,它的定义相当简单:

public interface ViewResolver {

    View resolveViewName(String viewName, Locale locale) throws Exception;
}

      
该接口只有一个方法,通过view name 解析出View。同样Spring提供了丰富的ViewResolver实现用来解析不同的View:

              《菜鸟之路——Spring MVC(五)ViewResolver》

   
ViewResolver的类结构:

《菜鸟之路——Spring MVC(五)ViewResolver》

  二、怎么获取ModelAndView

 
Spring是如何处理返回值并响应给客户呢?这就是这节要分析的,根据返回值解析出对应的视图。



<span style="font-size:14px;">private ModelAndView invokeHandleMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

        ServletWebRequest webRequest = new ServletWebRequest(request, response);

        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        ServletInvocableHandlerMethod requestMappingMethod = 
                                    createRequestMappingMethod(handlerMethod, binderFactory);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        AsyncExecutionChain chain = AsyncExecutionChain.getForCurrentRequest(request);
        chain.addDelegatingCallable(getAsyncCallable(mavContainer, modelFactory, webRequest));
        chain.setAsyncWebRequest(createAsyncWebRequest(request, response));
        chain.setTaskExecutor(this.taskExecutor);
        //调用了处理器方法并处理了返回值
        requestMappingMethod.invokeAndHandle(webRequest, mavContainer);

        if (chain.isAsyncStarted()) {
            return null;
        }
        //这里是根据返回值返回ModelAndView
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }</span>

     
invokeAndHandle方法调用了处理器方法,并处理了返回值,剩下的就是如何将返回值呈现给用户了,我们看getModelAndView的实现:



<span style="font-size:14px;">private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
            ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {
        //主要是同步model属性,并且将BindingResult添加到model中来
        modelFactory.updateModel(webRequest, mavContainer);
        //是否直接处理请求,如@ResponseBody
        if (mavContainer.isRequestHandled()) {
            return null;
        }
        ModelMap model = mavContainer.getModel();
        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);
        if (!mavContainer.isViewReference()) {
            mav.setView((View) mavContainer.getView());
        }//如果model是RedirectAttributes,进行flashAttributes的处理
        //即将flashAttribute属性添加到request的Output FlashMap中,以被重定向后的request获取
        if (model instanceof RedirectAttributes) {
            Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
            HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
            RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
        }
        return mav;
    }</span>

     
上面的代码是根据方法执行完后生成的model和视图名等信息生成ModelAndView对象,该对象维护了一个View和Model的对应关系,以便在View中可以访问Model的属性。
  
  RedirectAttributes:
 

上面的代码还有一个对RedirectAttributes的处理,这里我们来分析下是个什么回事?我们知道request中的属性只能在request范围内访问到,一旦执行重定向,重定向后的request并访问不到前面设置的属性了,虽然放到Session中可以在不同的request中共享这些属性,但是有时候放到Session中显得没有必要,毕竟很多属性只需要在“某次操作”中有用(重定向操作对用户来说其实是一次操作,因为重定向是浏览器执行的,对用户透明的)。
  因此为了解决这个问题,Spring引入了RedirectAttributes概念,即添加到RedirectAttributes中的属性,在重定向后依旧可以获取到,并且获取到以后,这些属性就会失效,新的request便无法获取了,这样就方便了开发者,同样也节省了内存占用。
  那Spring是怎么实现的呢?这里牵扯到了FlashMap这一概念,Spring会默认为每一个请求添加两个FlashMap属性,一个是InputFlashMap,另一个是OutputFlashMap,其中InputFlashMap便包含了上一个请求在重定向到该请求前设置的属性值,也就是上一个请求的OutputFlashMap,看下面的图方便理解:




        《菜鸟之路——Spring MVC(五)ViewResolver》
 
下面是DispatcherServlet中doService中的代码片段,在调用doDispatch前便设置了InputFlashmap和OutputFlashMap:



<span style="font-size:14px;">//尝试获取该request的InputFlashMap
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, 
                                                Collections.unmodifiableMap(inputFlashMap));
        }
//设置该请求的OutputFlashMap
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
//设置该请求的FlashMapManager,用来管理InputFlashMap和OutputFlashMap
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);</span>

  三、视图解析

 
了解了FlashMap的概念我们继续往下看,前面我们已经获取到了请求的ModelAndView对象,这时invokeHandleMethod执行完毕将控制权交给了doDispatch,我们看怎么处理ModelAndView:



<span style="font-size:14px;">mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncChain.isAsyncStarted()) {///异步调用,暂不关心
        mappedHandler.applyPostHandleAsyncStarted(processedRequest, response);
        return;
}//如果ModelAndView中没有设置视图名,则设置默认视图(大致是prefix/请求路径/suffix)
applyDefaultViewName(request, mv);
//执行拦截器的后处理器
mappedHandler.applyPostHandle(processedRequest, response, mv);
//处理分派结果,响应用户
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);</span>

     
  重点就在最后一行:

<span style="font-size:14px;">private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
            HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) {
        boolean errorView = false;
        //出现异常,进行异常处理,暂不关心
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }
        // 如果返回View需要渲染?
        if (mv != null && !mv.wasCleared()) {
            //惊醒视图的渲染,我们主题
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
        }
        //调用拦截器的afterComplete
        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }</span>

  
  上面的代码我们着重看render方法是怎样实现的:

<span style="font-size:14px;">protected void render(ModelAndView mv,HttpServletRequest request,HttpServletResponse response){
        // 确定当前请求的Locale,并设置Response
        Locale locale = this.localeResolver.resolveLocale(request);
        response.setLocale(locale);

        View view;//ModelAndView中的View还只是名称,需要解析成View对象
        if (mv.isReference()) {
            view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request);
            if (view == null) {
                throw new ServletException(
                        "Could not resolve view with name '");
            }
        }
        else {//直接获取视图对象
            view = mv.getView();
            if (view == null) {
                throw new ServletException("ModelAndView [" + mv + "] ");
            }
        }
        //委托视图对象进行渲染
        view.render(mv.getModelInternal(), request, response);
    }</span>

  
 
上面的代码涉及了两个重要步骤,视图名的解析和视图的渲染,这一小节我们来讲解视图名的解析,也就是ViewResolver了:

<span style="font-size:14px;">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;
    }</span>

      
resolverViewName方法的主要功能是根据ModelAndView中给定的viewName信息,再结合相关的配置,创建出合适类型的View对象。我们查看resolveViewName方法,发现其中有一个viewResolvers实例变量,它和之前讲的handlerMappings, handlerAdapters等变量一样,都是在DispatcherServlet初始化时完成设置的,在DispatcherServlet类中,init方法中已经进行了相关的初始化,配置的ViewResolver信息都存放在viewResolvers中。在render方法中调用resolverViewName方法,在这个方法中逐一调用ViewResolver去取得View对象。
  我们可以在配置文件中定义我们自己的HandleMappings, HandlerAdapters,ViewResolvers等。
  在Spring中,ViewResolver作为Spring Bean存在,可以在Spring配置文件中进行配置,例如下面的代码,配置了jsp相关的viewResolver:

<span style="font-size:14px;"><!-- Resolves view names to protected .jsp resources within the /WEB-INF/views directory -->  
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
        <property name="prefix" value="/WEB-INF/views/"/>  
        <property name="suffix" value=".jsp"/>  
    </bean>  </span>

        
在Spring MVC中,因为ViewResolver是使用bean来配置的,所以扩展起来非常的容易,可以根据自己的需要定制ViewResolver,然后在配置文件中进行相关的配置即可。但是如果我们不设置的话Spring也会为我们设置一些默认值:

<span style="font-size:14px;">org.springframework.web.servlet.HandlerMapping =
                org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
                org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=
                org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
                org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
                org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=
    org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=
                org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=
                            org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=
                    org.springframework.web.servlet.support.SessionFlashMapManager</span>

  
  上面代码片段来自Spring MVC包中的DispatcherServlet.properties属性文件中,这里Spring为我们默认设置了诸多处理器,解析器等,可以看出在我们不进行ViewResolver设置的情况下,默认实现是InternalResourceViewResolver。由第一节的ViewResolver继承层次图我们知道,InternalResourceViewResolver继承自UrlBasedViewResolver, 而UrlBasedViewResolver继承自AbstractCachingViewResolver,其实这就是Spring的聪明之处,为了提高性能,Spring中充斥着缓存策略,这不,在试图解析中也使用了缓存。这样只需在第一次解析时完成整个的视图创建工作,后续的请求只需从缓存中索取即可了。
 这里的InternalResourceViewResolver主要是用来支持Jsp文件的,换句话说,如果你的系统中只用到了jsp文件而没有模板引擎等框架,这个ViewResolver就够你用了,你也就无需在配置文件中多此一举的写上该ViewResolver了。下面我们就来看它的实现吧:

<span style="font-size:14px;">public View resolveViewName(String viewName, Locale locale) throws Exception {
        //如果没有被缓存呢,只能创建了
        if (!isCache()) {
            return createView(viewName, locale);
        }
        else {//检索缓存中的视图对象
            Object cacheKey = getCacheKey(viewName, locale);
            synchronized (this.viewCache) {
                View view = this.viewCache.get(cacheKey);
                if (view == null && (!this.cacheUnresolved 
                                                || !this.viewCache.containsKey(cacheKey))) {
                    // Ask the subclass to create the View object.
                    view = createView(viewName, locale);
                    if (view != null || this.cacheUnresolved) {
                        this.viewCache.put(cacheKey, view);
                    }
                }
                return view;
            }
        }
    }</span>

       
方法很简单,我们接着看是怎样创建视图的:

<span style="font-size:14px;">protected View createView(String viewName, Locale locale) throws Exception {
        // 当前ViewResolver无法解析该视图名,返回null
        if (!canHandle(viewName, locale)) {
            return null;
        }
        // view名称以redirect:开头,即重定向视图解析
        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);
        }
        // view名称以forward:开头,即转发视图解析
        if (viewName.startsWith(FORWARD_URL_PREFIX)) {
            String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
            return new InternalResourceView(forwardUrl);
        }
        // 正常情况下,由父类创建
        return super.createView(viewName, locale);
    }</span>

         
创建视图时,Spring会检查视图名,有三种情况redirect视图,forward视图,普通视图,进行了不同处理。对于redirect视图,spring获取redirectURL并创建了RedirectView对象,然后执行了一下bean实例的生命周期方法,没什么实质性东西,我们不关心。对于转发视图,创建了InternalResourceView对象,上面说的这两种对象的渲染过程我们过会会降到的。这里大家先记住。第三种情况呢,又交给了父类处理,我们继续看:

<span style="font-size:14px;">protected View createView(String viewName, Locale locale) throws Exception {
        return loadView(viewName, locale);
}
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
        AbstractUrlBasedView view = buildView(viewName);
        View result = applyLifecycleMethods(viewName, view);
        return (view.checkResource(locale) ? result : null);
}</span>

      
父类的createView方法又委托给了loadView,而loadView是抽象的由子类实现。我们继续看loadView中有一个buildView方法:

<span style="font-size:14px;">protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        //根据ViewClass实例化该Class
        AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils
                                                            .instantiateClass(getViewClass());
        //设置视图的url,prefix/viewName/suffix
        view.setUrl(getPrefix() + viewName + getSuffix());
        String contentType = getContentType();
        if (contentType != null) {//设置ContentType
            view.setContentType(contentType);
        }//设置请求上下文属性
        view.setRequestContextAttribute(getRequestContextAttribute());
        view.setAttributesMap(getAttributesMap());
        if (this.exposePathVariables != null) {//设置是否暴露PathVariable
            view.setExposePathVariables(exposePathVariables);
        }
        return view;
}</span>

        
上面的代码又出来个ViewClass, prefix,suffix,他们又是个什么东西呢?其实我们知道在配置InternalResourceViewResolver时可以指定一个viewClass,prefix,suffix,没错,就是他们,先说prefix,suffix,我们看到了它会分别添加到viewName的前后,组成视图的URL。那个viewClass呢就是视图的class对象类型了。我们看InternalResourceViewResolver的构造器:

<span style="font-size:14px;">public InternalResourceViewResolver() {
        Class viewClass = requiredViewClass();
        if (viewClass.equals(InternalResourceView.class) && jstlPresent) {
            viewClass = JstlView.class;
        }
        setViewClass(viewClass);
}</span>

 
  会发现在我们没有指定的情况下默认是JstlView,它继承自InternalResourceView。到此为止呢我们的视图对象已经创建完毕。
  我们这里只解析了Spring默认情况下的InternalResourceViewResolver的解析过程,默认情况下解析的视图类型是JstlView。如果是Redirect的话则是RedirectView。
  关于View对象的创建,不同的ViewResolver的解决方法是各部相同的。如BeanNameViewResolver是根据viewName选择相应名称的bean(这里需要注意bean的scope,是否需要线程安全),而UrlBasedViewResolver则是使用反射机制,根据viewClass信息创建view对象,因此这个view不受IoC容器的管理。ContentNegotiationViewResolver中可以嵌套ViewResolver,根据不同的的请求类型选择合适的ViewResolver。
  DispatcherServlet得到View对象后,即调用View的render方法,执行真正的渲染工作。

 四、视图渲染

 视图解析出来了,下面就是要将视图渲染给用户显示了。这里我们依旧只讲解默认的JstlView的渲染过程,当然还有RedirectView的。

<span style="font-size:14px;">public void render(Map<String, ?> model, HttpServletRequest request, 
                                                HttpServletResponse response) throws Exception {
        
        Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);

        prepareResponse(request, response);
        renderMergedOutputModel(mergedModel, request, response);
}
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, 
                            HttpServletRequest request, HttpServletResponse response) {
        @SuppressWarnings("unchecked")
        //如果需要保留PathVariable
        Map<String, Object> pathVars = this.exposePathVariables ?
            (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null;

        //联合动态和静态属性
        int size = this.staticAttributes.size();
        size += (model != null) ? model.size() : 0;
        size += (pathVars != null) ? pathVars.size() : 0;
        Map<String, Object> mergedModel = new HashMap<String, Object>(size);
        mergedModel.putAll(this.staticAttributes);
        if (pathVars != null) {
            mergedModel.putAll(pathVars);
        }
        if (model != null) {
            mergedModel.putAll(model);
        }
        // Expose RequestContext?
        if (this.requestContextAttribute != null) {
            mergedModel.put(this.requestContextAttribute, 
                                        createRequestContext(request, response, mergedModel));
        }
        
        return mergedModel;
}</span>

        
上面代码是AbstractView中的方法,也就是所有视图都会执行的操作,就是将静态属性和动态生成的属性合并,我们重点看renderMergedOutputModel方法,子类会覆盖该方法,实现不同的逻辑。我们来看JstlView和RedirectView的实现,首先JstlView:

<span style="font-size:14px;">protected void renderMergedOutputModel(
            Map<String, Object> model, HttpServletRequest request,HttpServletResponse response){

        //确定执行请求转发的request对象
        HttpServletRequest requestToExpose = getRequestToExpose(request);
        //将model中的属性暴露为请求属性表中
        exposeModelAsRequestAttributes(model, requestToExpose);
        //暴露MessageResource
        exposeHelpers(requestToExpose);
        //确定转发的路径,也就是View的URL,但会检查是否会进入死循环,即跟当前请求同一个路径
        String dispatcherPath = prepareForRendering(requestToExpose, response);
        //生成RequestDispatcher对象
        RequestDispatcher rd = getRequestDispatcher(requestToExpose, dispatcherPath);
        if (rd == null) {
            throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +"]");
        }
        //include操作
        if (useInclude(requestToExpose, response)) {
            response.setContentType(getContentType());
            rd.include(requestToExpose, response);
        }
        else {
            //执行转发,暴露属性到转发请求中
            exposeForwardRequestAttributes(requestToExpose);
            rd.forward(requestToExpose, response);
        }
}</span>

         
方法看着很长其实思路比较简单,主要就是调用了RequestDispatcher的include 或forward的方法,将请求转发到指定URL。JstlView的视图渲染相对简单,我们来看RedirectView的渲染:

<span style="font-size:14px;">protected void renderMergedOutputModel(
            Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        //获取重定向的路径,也就是前面生成RedirectView时设置的URL,但会进行相对路径的处理
        String targetUrl = createTargetUrl(model, request);
        //调用用户注册的RequestDataValueProcessor的process方法,通常用不到,不管
        targetUrl = updateTargetUrl(targetUrl, model, request, response);
        //这里就是我们上面讲到的FlashMap的处理,是怎样实现的呢?
        //我们知道前面将RedirectAttributes的属性都设置到了当前请求的OutputFlashMap中了,这里再取出来。
        //设置flashMap的目标请求路径,用来比对下次请求的路径,如果匹配,将其中的属性设置到请求属性表中
        FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
        if (!CollectionUtils.isEmpty(flashMap)) {
            UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
            flashMap.setTargetRequestPath(uriComponents.getPath());
            flashMap.addTargetRequestParams(uriComponents.getQueryParams());
        }
        //将flashMap交由FlashMapManager管理。
        FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
        flashMapManager.saveOutputFlashMap(flashMap, request, response);
        //返回结果,设置响应头304.
        sendRedirect(request, response, targetUrl.toString(), this.http10Compatible);
}</span>

      
到此为止,我们的试图解析,渲染过程就完全分析完了,Spring在视图解析,和渲染这块给了我们足够的拓展空间。由前面的View类结构图可知,Spring已经为我们提供了一系列可用的View。同时,如果当前提供的View不能满足我们的要求时,可以通过实现View接口进行扩展。如需要根据model中的数据使用JFreeChart绘图,或者将这些数据作为文件下载时,我们可以扩展出JFreeChartView和FileDownloadView等,这样就能更灵活的将同一份数据用不同的方式展现出来。

  

    原文作者:Spring MVC
    原文地址: https://blog.csdn.net/sjjsh2/article/details/53064554
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞