上一篇文章(1)(2)分析了Spring是如何調用和執行控制器方法,以及處理返回結果的,如今咱們就分析下Spring如何解析返回的結果生成響應的視圖。html
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
上一篇文章咱們分析了處理器方法如何被調用以及獲取了返回值,可是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
上面的代碼還有一個對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);
瞭解了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。
視圖解析出來了,下面就是要將視圖渲染給用戶顯示了。這裏咱們依舊只講解默認的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在視圖解析,和渲染這塊給了咱們足夠的拓展空間。
Spring對視圖的支持至關完善,默認的JSP不用說,PDF,Excel, 等,還包括主流的模板引擎,像FreeMarker, Tiles等,能夠參考第一張圖片。固然你徹底也能夠實現本身的View,以及ViewResolver,來解析自定義的視圖。不過應該沒多大必要。