在RequestMappingHandlerAdapter對request進行了適配,而且調用了目標handler以後,其會返回一個ModelAndView對象,該對象中主要封裝了兩個屬性:view和model。其中view能夠是字符串類型也能夠是View類型,若是是字符串類型,則表示邏輯視圖名,若是是View類型,則其即爲咱們要轉換的目標view;這裏model是一個Map類型的對象,其保存了渲染視圖所須要的屬性。本文主要講解Spring是如何經過用戶配置的ViewResolver來對視圖進行解析,而且聲稱頁面進行渲染的。前端
首先咱們來看一個比較典型的ViewResolver配置:java
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/view/"/> <property name="suffix" value=".jsp"/> </bean>
這裏配置的ViewResolver是InternalResourceViewResolver,其主要有兩個屬性:prefix和suffix。在進行視圖解析時,若是ModelAndView中的view是字符串類型的,那麼要解析的視圖存儲位置就經過「prefix + (String)view + suffix」的格式生成要解析的文件路徑,而且將其封裝爲一個View對象,最後經過View對象來渲染具體的視圖。前面講到,ModelAndView中view也能夠是View類型的,若是其是View類型的,那麼這裏就能夠跳過第一步,直接使用其提供的View對象進行視圖解析了。web
由上面的講解能夠看出,對於視圖的解析能夠分爲兩個步驟:①解析邏輯視圖名;②渲染視圖。對應於這兩步,Spring也抽象了兩個接口:ViewResolver和View,這兩個接口的聲明分別以下:spring
public interface ViewResolver { // 經過邏輯視圖名和用戶地區信息生成View對象 View resolveViewName(String viewName, Locale locale) throws Exception; }
public interface View { // 獲取返回值的contentType default String getContentType() { return null; } // 經過用戶提供的模型數據與視圖信息渲染視圖 void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception; }
從上面兩個接口的聲明能夠看出,ViewResolver的做用主要在於經過用戶提供的邏輯視圖名根據必定的策略生成一個View對象,而View接口則負責根據視圖信息和須要填充的模型數據進行視圖的渲染。這裏咱們首先看InternalResourceViewResolver是如何解析視圖名的,以下是其具體實現方式:瀏覽器
@Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { // 判斷當前ViewResolver是否設置了須要對須要解析的視圖進行緩存,若是不須要緩存, // 則每次請求時都會從新解析生成視圖對象 if (!isCache()) { // 根據視圖名稱和用戶地區信息建立View對象 return createView(viewName, locale); } else { // 若是能夠對視圖進行緩存,則首先獲取緩存使用的key,而後從緩存中獲取該key,若是沒有取到, // 則對其進行加鎖,再次獲取,若是仍是沒有取到,則建立一個新的View,而且對其進行緩存。 // 這裏使用的是雙檢查法來判斷緩存中是否存在對應的邏輯視圖。 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) { view = createView(viewName, locale); // 這裏cacheUnresolved指的是是否緩存默認的空視圖,UNRESOLVED_VIEW是 // 一個沒有任何內容的View 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主要是判斷了當前是否配置了須要緩存生成的View對象,若是須要緩存,則從緩存中取,若是沒有配置,則每次請求時都會從新生成新的View對象。這裏咱們繼續看其是如何建立視圖的:緩存
@Override protected View loadView(String viewName, Locale locale) throws Exception { // 使用邏輯視圖名按照指定規則生成View對象 AbstractUrlBasedView view = buildView(viewName); // 應用聲明周期函數,也就是調用View對象的初始化函數和Spring用於切入bean建立的 // Processor和Aware函數 View result = applyLifecycleMethods(viewName, view); // 檢查view的準確性,這裏默認始終返回true return (view.checkResource(locale) ? result : null); } // 這裏buildView()方法主要是根據邏輯視圖名生成一個View對象 protected AbstractUrlBasedView buildView(String viewName) throws Exception { // 對於InternalResourceViewResolver而言,其返回的View對象的 // 具體類型是InternalResourceView Class<?> viewClass = getViewClass(); Assert.state(viewClass != null, "No view class"); // 使用反射生成InternalResourceView對象實例 AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); // 這裏能夠看出,InternalResourceViewResolver獲取目標視圖的方式就是將用戶返回的 // viewName與prefix和suffix進行拼接,以供View對象直接讀取 view.setUrl(getPrefix() + viewName + getSuffix()); // 設置View的contentType屬性 String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } // 設置contextAttribute和attributeMap等屬性 view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); // 這了pathVariables表示request請求url中的屬性,這裏主要是設置是否將這些屬性暴露到視圖中 Boolean exposePathVariables = getExposePathVariables(); if (exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } // 這裏設置的是是否將Spring的bean暴露在視圖中,以供給前端調用 Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); if (exposeContextBeansAsAttributes != null) { view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); } // 設置須要暴露給前端頁面的bean名稱 String[] exposedContextBeanNames = getExposedContextBeanNames(); if (exposedContextBeanNames != null) { view.setExposedContextBeanNames(exposedContextBeanNames); } return view; } protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) { ApplicationContext context = getApplicationContext(); if (context != null) { // 對生成的View對象應用初始化方法,主要包括InitializingBean.afterProperties()和一些 // Processor,Aware方法 Object initialized = context.getAutowireCapableBeanFactory() .initializeBean(view, viewName); if (initialized instanceof View) { return (View) initialized; } } return view; }
從上面對於視圖名稱的解析,能夠看出,其主要作了四部分工做:①實例化View對象;②設置目標視圖地址;③初始化視圖的一些基本屬性,如須要暴露的bean對象;④調用View對象的初始化方法對其進行初始化。從這裏的生成View對象的過程也能夠看出,ViewResolver生成的View對象只是保存了目標view的地址,而對其加載和渲染的過程主要是委託給了View對象進行的。下面咱們就來看一下InternalResourceView是如何結合具體的model來渲染視圖的:app
@Override public void render(@Nullable 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); } // 這裏主要是將request中pathVariable,staticAttribute與用戶返回的model屬性 // 合併爲一個Map對象,以供給後面對視圖的渲染使用 Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); // 判斷當前View對象的類型是否爲文件下載類型,若是是文件下載類型,則設置response的 // Pragma和Cache-Control等屬性值 prepareResponse(request, response); // 經過合併的model數據以及視圖地址進行視圖的渲染 renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); }
這裏對於視圖的渲染主要分爲了三步:①合併用戶返回的model數據和request中的pathVariable與staticAttribute等數據;②判斷當前是否爲文件下載類型的視圖解析,若是是,則設置Pragma和Cache-Control等header;③經過合併的模型數據和request請求對視圖進行渲染。這裏咱們主要看一下renderMergedOutputModel()方法是如何對視圖進行渲染的:jsp
@Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // 這裏主要是對model進行遍歷,將其key和value設置到request中,當作request的 // 一個屬性供給頁面調用 exposeModelAsRequestAttributes(model, request); // 提供的一個hook方法,默認是空實現,用於用戶進行request屬性的自定義使用 exposeHelpers(request); // 檢查當前是否存在循環類型的視圖名稱解析,主要是根據相對路徑進行判斷視圖名是沒法解析的 String dispatcherPath = prepareForRendering(request, response); // 獲取當前request的RequestDispatcher對象,該對象有兩個方法:include()和forward(), // 用於對當前的request進行轉發,其實也就是將當前的request轉發到另外一個url,這裏的另外一個 // url就是要解析的視圖地址,也就是說進行視圖解析的時候請求的對於文件的解析實際上至關於 // 構造了另外一個(文件)請求,在該請求中對文件內容進行渲染,從而獲得最終的文件。這裏的 // include()方法表示將目標文件引入到當前文件中,與jsp中的include標籤做用相同; // forward()請求則表示將當前請求轉發到另外一個請求中,也就是目標文件路徑,這種轉發並不會 // 改變用戶瀏覽器地址欄的請求地址。 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!"); } // 判斷當前是否爲include請求,若是是,則調用RequestDispatcher.include()方法進行文件引入 if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } rd.include(request, response); } else { if (logger.isDebugEnabled()) { logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'"); } // 若是當前不是include()請求,則直接使用forward請求將當前請求轉發到目標文件路徑中, // 從而渲染該視圖 rd.forward(request, response); } }
上述代碼就是進行視圖渲染的核心邏輯,上述邏輯主要分爲兩個步驟:①將須要在頁面渲染使用的model數據設置到request中;②按照當前請求的方式(include或forward)來將當前請求轉發到目標文件中,從而達到目標文件的渲染。從這裏能夠看出,實際上對於Spring而言,其對頁面的渲染並非在其原始的request中完成的。ide
本文首先講解了Spring進行視圖渲染所須要的兩大組件ViewResolver和View的關係,而後以InternalResourceViewResolver和InternalResourceView爲例講解Spring底層是如何解析一個view,而且渲染該View的。函數