Spring MVC 解讀——View,ViewResolver

    上一篇文章(1)(2)分析了Spring是如何調用和執行控制器方法,以及處理返回結果的,如今咱們就分析下Spring如何解析返回的結果生成響應的視圖。html

1、概念理解

  •     View ---View接口表示一個響應給用戶的視圖,例如jsp文件,pdf文件,html文件等,它的定義以下
    java

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);
}

   該接口只有兩個方法定義,分別代表該視圖的ContentType和如何被渲染。Spring中提供了豐富的視圖支持,幾乎包含全部你想獲得的,而且Spring的視圖拓展性很好,你能夠輕鬆實現本身的視圖。下面是View的一些實現類(不是所有)ios

  • ViewResolver --- ViewResolver接口定義瞭如何經過view 名稱來解析對應View實例的行爲,它的定義至關簡單:web

public interface ViewResolver {

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

   該接口只有一個方法,經過view name 解析出View。一樣Spring提供了豐富的ViewResolver實現用來解析不一樣的View:spring

2、獲取ModelAndView

    上一篇文章咱們分析了處理器方法如何被調用以及獲取了返回值,可是Spring是如何處理返回值並響應給客戶呢?這就是這節要分析的,根據返回值解析出對應的視圖。瀏覽器

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);
    }

   上面的代碼在上一篇文章中已經分析到了invokeAndHandle方法,該方法調用了處理器方法,並處理了返回值,剩下的就是如何將返回值呈現給用戶了,咱們看getModelAndView的實現:緩存

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;
    }

   上面的代碼是根據方法執行完後生成的model和視圖名等信息生成ModelAndView對象,該對象維護了一個View和Model的對應關係,以便在View中能夠訪問Model的屬性。mvc

3、RedirectAttributes   

    上面的代碼還有一個對RedirectAttributes的處理,這裏咱們來分析下是個什麼回事?咱們知道request中的屬性只能在request範圍內訪問到,一旦執行重定向,重定向後的request並訪問不到前面設置的屬性了,雖然放到Session中能夠在不一樣的request中共享這些屬性,可是有時候放到Session中顯得沒有必要,畢竟不少屬性只須要在「某次操做」中有用(重定向操做對用戶來講實際上是一次操做,由於重定向是瀏覽器執行的,對用戶透明的。
app

    所以爲了解決這個問題,Spring引入了RedirectAttributes概念,即添加到RedirectAttributes中的屬性,在重定向後依舊能夠獲取到,而且獲取到之後,這些屬性就會失效,新的request便沒法獲取了,這樣就方便了開發者,一樣也節省了內錯佔用。框架

    那Spring是怎麼實現的呢?這裏牽扯到了FlashMap這一律念,Spring會默認爲每個請求添加兩個FlashMap屬性,一個是InputFlashMap,另外一個是OutputFlashMap,其中InputFlashMap便包含了上一個請求在重定向到該請求前設置的屬性值,也就是上一個請求的OutputFlashMap,看下面的圖方便理解:

    下面是DispatcherServlet中doService中的代碼片斷,在調用doDispatch前便設置了InputFlashmap和OutputFlashMap:

//嘗試獲取該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);

4、視圖解析

    瞭解了FlashMap的概念咱們繼續往下看,前面咱們已經獲取到了請求的ModelAndView對象,這時invokeHandleMethod執行完畢將控制權交給了doDispatch,咱們看怎麼處理ModelAndView:

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);

   重點就在最後一行,咱們繼續追蹤:

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);
        }
    }

   上面的代碼咱們着重看render方法是怎樣實現的,這是咱們今天的主題啊:

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);
    }

   上面的代碼涉及了兩個重要步驟,視圖名的解析和視圖的渲染,這一小節咱們來說解視圖名的解析,也就是ViewResolver了:

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;
    }

    咱們查看resolveViewName方法,發現其中有一個viewResolvers實例變量,若是你看過前面的幾篇文章,你獲取會記得handlerMappings, handlerAdapters等變量,不錯他們是一夥的,都是在DispatcherServlet初始化時完成設置的,而且咱們能夠在配置文件中定義咱們本身的HandleMappings, HandlerAdapters,ViewResolvers等(這裏不講解怎樣設置了),可是若是咱們不設置的話Spring也會爲咱們設置一些默認值:

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

   上面代碼片斷來自Spring MVC包中的DispatcherServlet.properties屬性文件中,這裏Spring爲咱們默認設置了諸多處理器,解析器等,能夠看出在咱們不進行ViewResolver設置的狀況下,默認實現是InternalResourceViewResolver。由第一節的ViewResolver繼承層次圖咱們知道,InternalResourceViewResolver繼承自UrlBasedViewResolver, 而UrlBasedViewResolver繼承自AbstractCachingViewResolver,其實這就是Spring的聰明之處,爲了提升性能,Spring中充斥着緩存策略,這不,在試圖解析中也使用了緩存。這樣只需在第一次解析時完成整個的視圖建立工做,後續的請求只需從緩存中索取便可了。

    這裏的InternalResourceViewResolver主要是用來支持Jsp文件的,換句話說,若是你的系統中只用到了jsp文件而沒有模板引擎等框架,這個ViewResolver就夠你用了,你也就無需在配置文件中畫蛇添足的寫上該ViewResolver了。下面咱們就來看它的實現吧:

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;
            }
        }
    }

   方法很簡單,咱們接着看是怎樣建立視圖的:

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);
    }

   建立視圖時,Spring會檢查視圖名,有三種狀況redirect視圖,forward視圖,普通視圖,進行了不一樣處理。對於redirect視圖,spring獲取redirectURL並建立了RedirectView對象,而後執行了一下bean實例的生命週期方法,沒什麼實質性東西,咱們不關心。對於轉發視圖,建立了InternalResourceView對象,上面說的這兩種對象的渲染過程咱們過會會降到的。這裏你們先記住。第三種狀況呢,又交給了父類處理,咱們繼續看看吧:

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);
}

   父類的createView方法又委託給了loadView,而loadView是抽象的由子類實現,好吧,我只能說這個地方真饒。咱們繼續看loadView中有一個buildView方法,看着不錯哦:

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;
}

   上面的代碼又出來個ViewClass, prefix,suffix,他們又是個什麼東西呢?其實咱們知道在配置InternalResourceViewResolver時能夠指定一個viewClass,prefix,suffix,沒錯,就是他們,先說prefix,suffix,咱們看到了它會分別添加到viewName的先後,組成視圖的URL。那個viewClass呢就是視圖的class對象類型了。咱們看InternalResourceViewResolver的構造器:

public InternalResourceViewResolver() {
        Class viewClass = requiredViewClass();
        if (viewClass.equals(InternalResourceView.class) && jstlPresent) {
            viewClass = JstlView.class;
        }
        setViewClass(viewClass);
}

   會發如今咱們沒有指定的狀況下默認是JstlView哦。根據第一季中的圖片咱們能夠知道它繼承自InternalResourceView。到此爲止呢咱們的視圖對象已經建立完畢。

    咱們這裏只解析了Spring默認狀況下的InternalResourceViewResolver的解析過程,默認狀況下解析的視圖類型是JstlView。若是是Redirect的話則是RedirectView。

5、視圖渲染

    視圖解析出來了,下面就是要將視圖渲染給用戶顯示了。這裏咱們依舊只講解默認的JstlView的渲染過程,固然還有RedirectView的。

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;
}

   上面代碼是AbstractView中的方法,也就是全部視圖都會執行的操做,就是將靜態屬性和動態生成的屬性合併,咱們重點看

renderMergedOutputModel方法,子類會覆蓋該方法,實現不一樣的邏輯。咱們來看JstlView和RedirectView的實現,首先JstlView :

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);
        }
}

  方法看着很長其實思路比較簡單,主要就是調用了RequestDispatcher的include 或forward的方法,將請求轉發到指定URL。JstlView的視圖渲染相對簡單,咱們來看RedirectView的渲染:

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);
}

   到此爲止,咱們的試圖解析,渲染過程就徹底分析完了,獲取到目前爲止有點暈,其實好好思考下,Spring在視圖解析,和渲染這塊給了咱們足夠的拓展空間。

6、總結

    Spring對視圖的支持至關完善,默認的JSP不用說,PDF,Excel, 等,還包括主流的模板引擎,像FreeMarker, Tiles等,能夠參考第一張圖片。固然你徹底也能夠實現本身的View,以及ViewResolver,來解析自定義的視圖。不過應該沒多大必要。

相關文章
相關標籤/搜索