SpringMVC原始碼關於檢視解析渲染

weixin_33860722發表於2018-09-08

之前我們完成了springmvc各元件的初始化,然後進入doDispatch()的方法,通過handlerMappings迭代出order優先順序最高的handlerMapping,然後呼叫handlerMapping的gethandler()方法獲取handlerExcutionChain物件,接著我們通過getHandlerAdapter()方法,傳入handler作為引數,獲得匹配的handlerAdapter,獲取到handlerExcutionChain和handlerAdapter後,我們呼叫介面卡的handler(req,res,handler)的方式獲取ModelAndView物件,這是前幾天完成的部分。以下引用連結https://blog.csdn.net/wangbiao007/article/details/50510274 來展示整個流程

13278348-5e1e89fa45cad26f.png
image.png

通過ModelAndView我們獲取到了model和viewName,程式接著往下走,到方法applyDefaultViewName(processedRequest, mv)的呼叫,這個方法內部如下:

    private void applyDefaultViewName(HttpServletRequest request, ModelAndView mv) throws Exception {
            if (mv != null && !mv.hasView()) {
                mv.setViewName(getDefaultViewName(request));
            }
        }

如果先前我們獲取到mv,並且mv是有viewName的,那麼這個方法沒有用,因為這個方法就是在我們有mv但是沒有viewName的情況下,給我們設定一個預設的viewName

程式接著往下走,到方法mappedHandler.applyPostHandle(processedRequest, response, mv)的呼叫,這個內部如下:

    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, ModelAndView mv) throws Exception {
            HandlerInterceptor[] interceptors = getInterceptors();
            if (!ObjectUtils.isEmpty(interceptors)) {
                for (int i = interceptors.length - 1; i >= 0; i--) {
                    HandlerInterceptor interceptor = interceptors[i];
                    interceptor.postHandle(request, response, this.handler, mv);
                }
            }
        }

獲取到專案的攔截器,然後判斷攔截器是否存在,存在則對該請求進行測試(postHandler),如果有一個攔截器不成功,則請求失敗。

程式接著往下走,到本次最重要的檢視解析和渲染部分,也就是呼叫方法processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException)
該方法的程式碼如下:

    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws 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);
                }
            }
    
            // Did the handler return a view to render?
            if (mv != null && !mv.wasCleared()) {
                render(mv, request, response);
                if (errorView) {
                    WebUtils.clearErrorRequestAttributes(request);
                }
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                            "': assuming HandlerAdapter completed request handling");
                }
            }
    
            if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // Concurrent handling started during a forward
                return;
            }
    
            if (mappedHandler != null) {
                mappedHandler.triggerAfterCompletion(request, response, null);
            }
        }

首先判斷一下是否異常,如果出現異常,判斷異常是否是ModelAndViewDefiningException的例項,如果是就直接呼叫getModelAndView方法返回一個mv,否則呼叫processHanlderException方法初始化mv物件

如果沒有異常出現,那麼判斷mv是否為空,為空丟擲異常,不為空則判斷mv,wasCleared()方法返回的值是否為真,我們來看下這個方法的實現:

    public boolean wasCleared() {
            return (this.cleared && isEmpty());
        }

首先這個方法存在於ModelAndView類中,cleared的預設值設定為false,如果執行了clear()方法,則cleared的值置為true,接著是isEmpty()方法判斷:

  public boolean isEmpty() {
    return (this.view == null && CollectionUtils.isEmpty(this.model));
}

判斷完成後,呼叫DispatcherServlet的render(mv,req,res)方法,程式碼如下:

    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 {
                if (mv.getStatus() != null) {
                    response.setStatus(mv.getStatus().value());
                }
                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;
            }
        }

首先介紹一下Locale物件,設定整個系統的執行語言,然後根據系統的執行語言來展示對應語言的頁面,平時我們總會出現亂碼的情況,那麼可以看下response的語言型別到底是否是我們設定的情況。

這個方法中有兩部分,第一部分是解析檢視,在if(mv.isReference()){}裡頭,第二部分是檢視渲染,在try{}catch{}程式碼塊中的,view.render(mv.getModelInternal(),req,res)
首先介紹一下第一部分解析檢視,mv.isReference()是判斷viewName是否存在,存在,則通過resolveViewName(mv.getViewName,mv.getModelInternal,locale,request)方法進行解析,該方法的程式碼如下:

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

迭代viewResolverss物件,然後呼叫viewResolver的resolverViewName(viewName,locale)方法,springMVC給我們提供了多種的resolverViewName的方法實現


13278348-1c4d7637577cd620.png
image.png

通過這些實現來返回我們的view物件。

如果之前判斷isReference為false,則直接呼叫view.getView的方法獲取view。

解析成功view之後,就是執行渲染操作了,渲染操作呼叫剛剛解析獲得的view的render方法,該方法的內部實現如下:

    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, getRequestToExpose(request), response);
        }

首先先獲取到需要加入到request的model的map集合,呼叫方法createMergedOutputModel方法,然後呼叫prepareResponse(request,response),該方法是設定響應請求頭的兩個引數,程式碼如下:

    protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
            if (generatesDownloadContent()) {
                response.setHeader("Pragma", "private");
                response.setHeader("Cache-Control", "private, must-revalidate");
            }
        }       

最後一步就是呼叫renderMergedOutputModel()方法去將mergeModel合到請求裡,該方法的程式碼實現如下:

    protected void renderMergedOutputModel(
                Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
            // Expose the model object as request attributes.
            exposeModelAsRequestAttributes(model, request);
    
            // Expose helpers as request attributes, if any.
            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.
                if (logger.isDebugEnabled()) {
                    logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
                }
                rd.forward(request, response);
            }
        }

前面四行都是配置requestDispatcher用的,我主要看了下方法exposeModelAsRequestAttributes(),程式碼如下:

    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() + "'");
                    }
                }
            }
        }

發現了什麼,我們不使用框架的時候,如果想要將model傳到前端,就是需要使用request.setAttribute()的方法,也就是這個方法封裝了我們對於model到前端的操作。

RequestDispatcher 物件配置完成後,就是選擇它要進行的操作了,是使用include還是forward進行跳轉。至此,檢視的解析與渲染工作都完成了,並且也傳送到了前端頁面,完成了一個請求的整個流程。

總結
在獲取到ModelAndView物件後,先判斷在mv存在的情況下viewName是否存在,不存在就給定一個初始值,之後對請求進行攔截判斷,如果有一個不通過則請求失敗,都通過則進行下一步,呼叫DispatcherServlet.render()方法進行檢視的解析和渲染,呼叫resolverViewName()方法迭代viewResolvers,如果viewResolver.resolveViewName(viewName, locale)的返回結果不為空,則返回該結果,解析成功。
然後再進行檢視的渲染工作,通過前面獲取到的view,呼叫該物件的render(model,req,res)方法,方法內先獲取該請求的Controller方法中設定的model,然後設定響應請求頭,最後呼叫renderMergedOutputModel將model放到request中通過requestDispatcher.include(req,res)或者requestDispatcher.forward(req,res)的方式傳到前端,展現給使用者。

參考連結:
https://www.cnblogs.com/solverpeng/p/5743609.html
https://blog.csdn.net/qq924862077/article/details/52878507
https://www.jianshu.com/p/5f91b6d7591a
https://blog.csdn.net/prince2270/article/details/5891085

相關文章