1. 程式人生 > >精盡Spring MVC原始碼分析 - ViewResolver 元件

精盡Spring MVC原始碼分析 - ViewResolver 元件

> 該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 [Spring MVC 原始碼分析 GitHub 地址](https://github.com/liu844869663/spring-framework) 進行閱讀 > > Spring 版本:5.2.4.RELEASE > > 該系列其他文件請檢視:[**《精盡 Spring MVC 原始碼分析 - 文章導讀》**](https://www.cnblogs.com/lifullmoon/p/14123963.html) ## ViewResolver 元件 `ViewResolver` 元件,檢視解析器,根據檢視名和國際化,獲得最終的檢視 View 物件 ### 回顧 先來回顧一下在 `DispatcherServlet` 中處理請求的過程中哪裡使用到 `ViewResolver` 元件,可以回到[**《一個請求的旅行過程》**](https://www.cnblogs.com/lifullmoon/p/14131862.html)中的 `DispatcherServlet` 的 `render` 方法中看看,如下: ```java protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. // <1> 解析 request 中獲得 Locale 物件,並設定到 response 中 Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); // 獲得 View 物件 View view; String viewName = mv.getViewName(); // 情況一,使用 viewName 獲得 View 物件 if (viewName != null) { // We need to resolve the view name. // <2.1> 使用 viewName 獲得 View 物件 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { // 獲取不到,丟擲 ServletException 異常 throw new ServletException("Could not resolve view with name '" + mv.getViewName() + "' in servlet with name '" + getServletName() + "'"); } } // 情況二,直接使用 ModelAndView 物件的 View 物件 else { // No need to lookup: the ModelAndView object contains the actual View object. // 直接使用 ModelAndView 物件的 View 物件 view = mv.getView(); if (view == null) { // 獲取不到,丟擲 ServletException 異常 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.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] "); } try { // <3> 設定響應的狀態碼 if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } // <4> 渲染頁面 view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]", ex); } throw ex; } } @Nullable protected View resolveViewName(String viewName, @Nullable Map model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { // 遍歷 ViewResolver 陣列 for (ViewResolver viewResolver : this.viewResolvers) { // 根據 viewName + locale 引數,解析出 View 物件 View view = viewResolver.resolveViewName(viewName, locale); // 解析成功,直接返回 View 物件 if (view != null) { return view; } } } return null; } ``` 如果 ModelAndView 物件不為`null`,且需要進行頁面渲染,則呼叫 `render` 方法,如果設定的 View 物件是 `String` 型別,也就是 `viewName`,則需要呼叫 `resolveViewName` 方法,通過 `ViewResolver` 根據 `viewName` 和 `locale` 解析出對應的 View 物件 這是前後端未分離的情況下重要的一個元件 ### ViewResolver 介面 `org.springframework.web.servlet.ViewResolver`,檢視解析器,根據檢視名和國際化,獲得最終的檢視 View 物件,程式碼如下: ```java public interface ViewResolver { /** * 根據檢視名和國際化,獲得最終的 View 物件 */ @Nullable View resolveViewName(String viewName, Locale locale) throws Exception; } ``` ViewResolver 介面體系的結構如下:
ViewResolver 的實現類比較多,其中 Spring MVC 預設使用 `org.springframework.web.servlet.view.InternalResourceViewResolver` 這個實現類 Spring Boot 中的預設實現類如下: 可以看到有三個實現類: - `org.springframework.web.servlet.view.ContentNegotiatingViewResolver` - `org.springframework.web.servlet.view.ViewResolverComposite`,預設沒有實現類 - `org.springframework.web.servlet.view.BeanNameViewResolver` - `org.springframework.web.servlet.view.InternalResourceViewResolver` ### 初始化過程 在 `DispatcherServlet` 的 `initViewResolvers(ApplicationContext context)` 方法,初始化 ViewResolver 元件,方法如下: ```java private void initViewResolvers(ApplicationContext context) { // 置空 viewResolvers 處理 this.viewResolvers = null; // 情況一,自動掃描 ViewResolver 型別的 Bean 們 if (this.detectAllViewResolvers) { // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); if (!matchingBeans.isEmpty()) { this.viewResolvers = new ArrayList<>(matchingBeans.values()); // We keep ViewResolvers in sorted order. AnnotationAwareOrderComparator.sort(this.viewResolvers); } } // 情況二,獲得名字為 VIEW_RESOLVER_BEAN_NAME 的 Bean 們 else { try { ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); this.viewResolvers = Collections.singletonList(vr); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default ViewResolver later. } } // Ensure we have at least one ViewResolver, by registering // a default ViewResolver if no other resolvers are found. /** * 情況三,如果未獲得到,則獲得預設配置的 ViewResolver 類 * {@link org.springframework.web.servlet.view.InternalResourceViewResolver} */ if (this.viewResolvers == null) { this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); if (logger.isTraceEnabled()) { logger.trace("No ViewResolvers declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } } ``` 1. 如果“開啟”探測功能,則掃描已註冊的 ViewResolver 的 Bean 們,新增到 `viewResolvers` 中,預設**開啟** 2. 如果“關閉”探測功能,則獲得 Bean 名稱為 "viewResolver" 對應的 Bean ,將其新增至 `viewResolvers` 3. 如果未獲得到,則獲得預設配置的 ViewResolver 類,呼叫 `getDefaultStrategies(ApplicationContext context, Class strategyInterface)` 方法,就是從 `DispatcherServlet.properties` 檔案中讀取 ViewResolver 的預設實現類,如下: ```properties org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver ``` 在 Spring Boot 不是通過這樣初始化的,感興趣的可以去看看 ### ContentNegotiatingViewResolver `org.springframework.web.servlet.view.ContentNegotiatingViewResolver`,實現 ViewResolver、Ordered、InitializingBean 介面,繼承 WebApplicationObjectSupport 抽象類,基於**內容型別**來獲取對應 View 的 ViewResolver 實現類。其中,**內容型別**指的是 `Content-Type` 和拓展字尾 #### 構造方法 ```java public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean { @Nullable private ContentNegotiationManager contentNegotiationManager; /** * ContentNegotiationManager 的工廠,用於建立 {@link #contentNegotiationManager} 物件 */ private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); /** * 在找不到 View 物件時,返回 {@link #NOT_ACCEPTABLE_VIEW} */ private boolean useNotAcceptableStatusCode = false; /** * 預設 View 陣列 */ @Nullable private List defaultViews; /** * ViewResolver 陣列 */ @Nullable private List viewResolvers; /** * 順序,優先順序最高 */ private int order = Ordered.HIGHEST_PRECEDENCE; } ``` - `viewResolvers`:ViewResolver 陣列。對於來說,ContentNegotiatingViewResolver 會使用這些 ViewResolver們,解析出所有的 View 們,然後基於**內容型別**,來獲取對應的 View 們。此時的 View 結果,可能是一個,可能是多個,所以需要比較獲取到**最優**的 View 物件。 - `defaultViews`:預設 View 陣列。那麼此處的預設是什麼意思呢?在 `viewResolvers` 們解析出所有的 View 們的基礎上,也會新增 `defaultViews` 到 View 結果中 - `order`:順序,優先順序**最高**。所以,這也是為什麼它排在最前面 在上圖中可以看到,在 Spring Boot 中 `viewResolvers` 屬性有三個實現類,分別是 `BeanNameViewResolver`、`ViewResolverComposite`、`InternalResourceViewResolver` #### initServletContext 實現 `initServletContext(ServletContext servletContext)` 方法,初始化 `viewResolvers` 屬性,方法如下: > 在父類 WebApplicationObjectSupport 的父類 ApplicationObjectSupport 中可以看到,因為實現了 ApplicationContextAware 介面,則在初始化該 Bean 的時候會呼叫 `setApplicationContext(@Nullable ApplicationContext context)` 方法,在這個方法中會呼叫 `initApplicationContext(ApplicationContext context)` 這個方法,這個方法又會呼叫`initServletContext(ServletContext servletContext)` 方法 ```java @Override protected void initServletContext(ServletContext servletContext) { // <1> 掃描所有 ViewResolver 的 Bean 們 Collection matchingBeans = BeanFactoryUtils. beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values(); // <1.1> 情況一,如果 viewResolvers 為空,則將 matchingBeans 作為 viewResolvers 。 // BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver if (this.viewResolvers == null) { this.viewResolvers = new ArrayList<>(matchingBeans.size()); for (ViewResolver viewResolver : matchingBeans) { if (this != viewResolver) { // 排除自己 this.viewResolvers.add(viewResolver); } } } // <1.2> 情況二,如果 viewResolvers 非空,則和 matchingBeans 進行比對,判斷哪些未進行初始化,進行初始化 else { for (int i = 0; i < this.viewResolvers.size(); i++) { ViewResolver vr = this.viewResolvers.get(i); // 已存在在 matchingBeans 中,說明已經初始化,則直接 continue if (matchingBeans.contains(vr)) { continue; } // 不存在在 matchingBeans 中,說明還未初始化,則進行初始化 String name = vr.getClass().getName() + i; obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name); } } // <1.3> 排序 viewResolvers 陣列 AnnotationAwareOrderComparator.sort(this.viewResolvers); // <2> 設定 cnmFactoryBean 的 servletContext 屬性 this.cnmFactoryBean.setServletContext(servletContext); } ``` 1. 掃描所有 ViewResolver 的 Bean 們 `matchingBeans` 1. 情況一,如果 `viewResolvers` 為空,則將 `matchingBeans` 作為 `viewResolvers` 2. 情況二,如果 `viewResolvers` 非空,則和 `matchingBeans` 進行比對,判斷哪些未進行初始化,進行初始化 3. 排序 `viewResolvers` 陣列 2. 設定 `cnmFactoryBean` 的 `servletContext` 屬性為當前 Servlet 上下文 #### afterPropertiesSet 因為 ContentNegotiatingViewResolver 實現了 InitializingBean 介面,在 Sping 初始化該 Bean 的時候,會呼叫該方法,完成一些初始化工作,方法如下: ```java @Override public void afterPropertiesSet() { // 如果 contentNegotiationManager 為空,則進行建立 if (this.contentNegotiationManager == null) { this.contentNegotiationManager = this.cnmFactoryBean.build(); } if (this.viewResolvers == null || this.viewResolvers.isEmpty()) { logger.warn("No ViewResolvers configured"); } } ``` #### resolveViewName 實現 `resolveViewName(String viewName, Locale locale)` 方法,程式碼如下: ```java @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes"); // <1> 獲得 MediaType 陣列 List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { // <2> 獲得匹配的 View 陣列 List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); // <3> 篩選最匹配的 View 物件 View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); // 如果篩選成功,則返回 if (bestView != null) { return bestView; } } String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : ""; // <4> 如果匹配不到 View 物件,則根據 useNotAcceptableStatusCode ,返回 NOT_ACCEPTABLE_VIEW 或 null if (this.useNotAcceptableStatusCode) { if (logger.isDebugEnabled()) { logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo); } return NOT_ACCEPTABLE_VIEW; } else { logger.debug("View remains unresolved" + mediaTypeInfo); return null; } } ``` 1. 呼叫 `getMediaTypes(HttpServletRequest request)` 方法,獲得 MediaType 陣列,詳情見下文 2. 呼叫 `getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)` 方法,獲得匹配的 View 陣列,詳情見下文 3. 呼叫 `getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs)` 方法,篩選出最匹配的 View 物件,如果篩選成功則直接返回,詳情見下文 4. 如果匹配不到 View 物件,則根據 `useNotAcceptableStatusCode`,返回 `NOT_ACCEPTABLE_VIEW` 或 `null`,其中`NOT_ACCEPTABLE_VIEW` 變數,程式碼如下: ```java private static final View NOT_ACCEPTABLE_VIEW = new View() { @Override @Nullable public String getContentType() { return null; } @Override public void render(@Nullable Map model, HttpServletRequest request, HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); } }; ``` 這個 View 物件設定狀態碼為 `406` #### getMediaTypes `getCandidateViews(HttpServletRequest request)`方法,獲得 MediaType 陣列,如下: ```java @Nullable protected List getMediaTypes(HttpServletRequest request) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); try { // 建立 ServletWebRequest 物件 ServletWebRequest webRequest = new ServletWebRequest(request); // 從請求中,獲得可接受的 MediaType 陣列。預設實現是,從請求頭 ACCEPT 中獲取 List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest); // 獲得可產生的 MediaType 陣列 List producibleMediaTypes = getProducibleMediaTypes(request); // 通過 acceptableTypes 來比對,將符合的 producibleType 新增到 mediaTypesToUse 結果陣列中 Set compatibleMediaTypes = new LinkedHashSet<>(); for (MediaType acceptable : acceptableMediaTypes) { for (MediaType producible : producibleMediaTypes) { if (acceptable.isCompatibleWith(producible)) { compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); } } } // 按照 MediaType 的 specificity、quality 排序 List selectedMediaTypes = new ArrayList<>(compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(selectedMediaTypes); return selectedMediaTypes; } catch (HttpMediaTypeNotAcceptableException ex) { if (logger.isDebugEnabled()) { logger.debug(ex.getMessage()); } return null; } } ``` #### getCandidateViews `getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)`方法,獲得匹配的 View 陣列,如下: ```java private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) throws Exception { // 建立 View 陣列 List candidateViews = new ArrayList<>(); // <1> 來源一,通過 viewResolvers 解析出 View 陣列結果,新增到 candidateViews 中 if (this.viewResolvers != null) { Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set"); // <1.1> 遍歷 viewResolvers 陣列 for (ViewResolver viewResolver : this.viewResolvers) { // <1.2> 情況一,獲得 View 物件,新增到 candidateViews 中 View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { candidateViews.add(view); } // <1.3> 情況二,帶有拓展字尾的方式,獲得 View 物件,新增到 candidateViews 中 for (MediaType requestedMediaType : requestedMediaTypes) { // <1.3.2> 獲得 MediaType 對應的拓展字尾的陣列(預設情況下未配置) List extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); // <1.3.3> 遍歷拓展字尾的陣列 for (String extension : extensions) { // <1.3.4> 帶有拓展字尾的方式,獲得 View 物件,新增到 candidateViews 中 String viewNameWithExtension = viewName + '.' + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); if (view != null) { candidateViews.add(view); } } } } } // <2> 來源二,新增 defaultViews 到 candidateViews 中 if (!CollectionUtils.isEmpty(this.defaultViews)) { candidateViews.addAll(this.defaultViews); } return candidateViews; } ``` 1. 來源一,通過 `viewResolvers` 解析出 View 陣列結果,新增到 `List candidateViews` 中 1. 遍歷 `viewResolvers` 陣列 2. 情況一,通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中 3. 情況二,遍歷入參 `List requestedMediaTypes`,將帶有拓展字尾的型別再通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中 2. 獲得 MediaType 對應的拓展字尾的陣列(預設情況下未配置) 3. 遍歷拓展字尾的陣列 4. 帶有拓展字尾的方式,通過當前 ViewResolver 實現類獲得 View 物件,新增到 `candidateViews` 中 2. 來源二,新增 `defaultViews` 到 `candidateViews` 中 #### getBestView `getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs)`方法,篩選出最匹配的 View 物件,如下: ```java @Nullable private View getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs) { // <1> 遍歷 candidateView 陣列,如果有重定向的 View 型別,則返回它 for (View candidateView : candidateViews) { if (candidateView instanceof SmartView) { SmartView smartView = (SmartView) candidateView; if (smartView.isRedirectView()) { return candidateView; } } } // <2> 遍歷 MediaType 陣列(MediaTy陣列已經根據pespecificity、quality進行了排序) for (MediaType mediaType : requestedMediaTypes) { // <2> 遍歷 View 陣列 for (View candidateView : candidateViews) { if (StringUtils.hasText(candidateView.getContentType())) { // <2.1> 如果 MediaType 型別匹配,則返回該 View 物件 MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); if (mediaType.isCompatibleWith(candidateContentType)) { if (logger.isDebugEnabled()) { logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes); } attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST); return candidateView; } } } } return null; } ``` 1. 遍歷 `candidateView` 陣列,如果有**重定向**的 View 型別,則返回它。也就是說,**重定向**的 View ,優先順序更高。 2. 遍歷 MediaType 陣列(MediaTy陣列已經根據`pespecificity`、`quality`進行了排序)和 `candidateView` 陣列 1. 如果 MediaType 型別匹配該 View 物件,則返回該 View 物件。也就是說,優先順序的匹配規則,由 ViewResolver 在 `viewResolvers` 的位置,越靠前,優先順序越高。 ### BeanNameViewResolver `org.springframework.web.servlet.view.BeanNameViewResolver`,實現 ViewResolver、Ordered 介面,繼承 WebApplicationObjectSupport 抽象類,基於 Bean 的名字獲得 View 物件的 ViewResolver 實現類 #### 構造方法 ```java public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { /** * 順序,優先順序最低 */ private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered } ``` #### resolveViewName 實現 `resolveViewName(String viewName, Locale locale)` 方法,根據名稱獲取 View 型別對應的 Bean(View 物件),如下: ```java @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws BeansException { ApplicationContext context = obtainApplicationContext(); // 如果對應的 Bean 物件不存在,則返回 null if (!context.containsBean(viewName)) { // Allow for ViewResolver chaining... return null; } // 如果 Bean 對應的 Bean 型別不是 View ,則返回 null if (!context.isTypeMatch(viewName, View.class)) { if (logger.isDebugEnabled()) { logger.debug("Found bean named '" + viewName + "' but it does not implement View"); } // Since we're looking into the general ApplicationContext here, // let's accept this as a non-match and allow for chaining as well... return null; } // 獲得 Bean 名字對應的 View 物件 return context.getBean(viewName, View.class); } ``` ### ViewResolverComposite `org.springframework.web.servlet.view.ViewResolverComposite`,實現 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 介面,複合的 ViewResolver 實現類 #### 構造方法 ```java public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean, ApplicationContextAware, ServletContextAware { /** * ViewResolver 陣列 */ private final List viewResolvers = new ArrayList<>(); /** * 順序,優先順序最低 */ private int order = Ordered.LOWEST_PRECEDENCE; } ``` #### afterPropertiesSet 因為 ViewResolverComposite 實現了 InitializingBean 介面,在 Sping 初始化該 Bean 的時候,會呼叫該方法,完成一些初始化工作,方法如下: ```java @Override public void afterPropertiesSet() throws Exception { for (ViewResolver viewResolver : this.viewResolvers) { if (viewResolver instanceof InitializingBean) { ((InitializingBean) viewResolver).afterPropertiesSet(); } } } ``` #### resolveViewName 實現 `resolveViewName(String viewName, Locale locale)` 方法,程式碼如下: ```java @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { // 遍歷 viewResolvers 陣列,逐個進行解析,但凡成功,則返回該 View 物件 for (ViewResolver viewResolver : this.viewResolvers) { // 執行解析 View view = viewResolver.resolveViewName(viewName, locale); // 解析成功,則返回該 View 物件 if (view != null) { return view; } } return null; } ``` ### AbstractCachingViewResolver `org.springframework.web.servlet.view.AbstractCachingViewResolver`,實現 ViewResolver 介面,繼承 WebApplicationObjectSupport 抽象類,提供通用的**快取**的 ViewResolver 抽象類。對於相同的檢視名,返回的是相同的 View 物件,所以通過快取,可以進一步提供效能。 #### 構造方法 ```java public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver { /** Default maximum number of entries for the view cache: 1024. */ public static final int DEFAULT_CACHE_LIMIT = 1024; /** Dummy marker object for unresolved views in the cache Maps. */ private static final View UNRESOLVED_VIEW = new View() { @Override @Nullable public String getContentType() { return null; } @Override public void render(@Nullable Map model, HttpServletRequest request, HttpServletResponse response) { } }; /** The maximum number of entries in the cache. */ private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; // 快取上限。如果 cacheLimit = 0 ,表示禁用快取 /** Whether we should refrain from resolving views again if unresolved once. */ private boolean cacheUnresolved = true; // 是否快取空 View 物件 /** Fast access cache for Views, returning already cached instances without a global lock. */ private final Map viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT); // View 的快取的對映 /** Map from view key to View instance, synchronized for View creation. */ // View 的快取的對映。相比 {@link #viewAccessCache} 來說,增加了 synchronized 鎖 @SuppressWarnings("serial") private final Map viewCreationCache = new LinkedHashMap(DEFAULT_CACHE_LIMIT, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { if (size() > getCacheLimit()) { viewAccessCache.remove(eldest.getKey()); return true; } else { return false; } } }; } ``` 通過 `viewAccessCache` 屬性,提供更快的訪問 View 快取 通過 `viewCreationCache` 屬性,提供快取的上限的功能 KEY 是通過 `getCacheKey(String viewName, Locale locale)` 方法,獲得快取 KEY,方法如下: ```java protected Object getCacheKey(String viewName, Locale locale) { return viewName + '_' + locale; } ``` #### resolveViewName 實現 `resolveViewName(String viewName, Locale locale)` 方法,程式碼如下: ```java @Override @Nullable public View resolveViewName(String viewName, Locale locale) throws Exception { // 如果禁用快取,則建立 viewName 對應的 View 物件 if (!isCache()) { return createView(viewName, locale); } else { // 獲得快取 KEY Object cacheKey = getCacheKey(viewName, locale); // 從 viewAccessCache 快取中,獲得 View 物件 View view = this.viewAccessCache.get(cacheKey); // 如果獲得不到快取,則從 viewCreationCache 中,獲得 View 物件 if (view == null) { synchronized (this.viewCreationCache) { // 從 viewCreationCache 中,獲得 View 物件 view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. // 建立 viewName 對應的 View 物件 view = createView(viewName, locale); // 如果建立失敗,但是 cacheUnresolved 為 true ,則設定為 UNRESOLVED_VIEW if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } // 如果 view 非空,則新增到 viewAccessCache 快取中 if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); } } } } else { if (logger.isTraceEnabled()) { logger.trace(formatKey(cacheKey) + "served from cache"); } } return (view != UNRESOLVED_VIEW ? view : null); } } @Nullable protected View createView(String viewName, Locale locale) throws Exception { return loadView(viewName, locale); } @Nullable protected abstract View loadView(String viewName, Locale locale) throws Exception; ``` 邏輯比較簡單,主要是快取的處理,需要通過子類去建立對應的 View 物件 ### UrlBasedViewResolver `org.springframework.web.servlet.view.UrlBasedViewResolver`,實現 Ordered 介面,繼承 AbstractCachingViewResolver 抽象類,基於 Url 的 ViewResolver 實現類 #### 構造方法 ```java public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered { public static final String REDIRECT_URL_PREFIX = "redirect:"; public static final String FORWARD_URL_PREFIX = "forward:"; /** * View 的型別,不同的實現類,會對應一個 View 的型別 */ @Nullable private Class viewClass; /** * 字首 */ private String prefix = ""; /** * 字尾 */ private String suffix = ""; /** * ContentType 型別 */ @Nullable private String contentType; private boolean redirectContextRelative = true; private boolean redirectHttp10Compatible = true; @Nullable private String[] redirectHosts; /** * RequestAttributes 暴露給 View 使用時的屬性 */ @Nullable private String requestContextAttribute; /** Map of static attributes, keyed by attribute name (String). */ private final Map staticAttributes = new HashMap<>(); /** * 是否暴露路徑變數給 View 使用 */ @Nullable private Boolean exposePathVariables; @Nullable private Boolean exposeContextBeansAsAttributes; @Nullable private String[] exposedContextBeanNames; /** * 是否只處理指定的檢視名們 */ @Nullable private String[] viewNames; /** * 順序,優先順序最低 */ private int order = Ordered.LOWEST_PRECEDENCE; } ``` #### initApplicationContext 實現 `initApplicationContext()` 方法,進一步初始化,程式碼如下: > 在父類 WebApplicationObjectSupport 的父類 ApplicationObjectSupport 中可以看到,因為實現了 ApplicationContextAware 介面,則在初始化該 Bean 的時候會呼叫 `setApplicationContext(@Nullable ApplicationContext context)` 方法,在這個方法中會呼叫 `initApplicationContext(ApplicationContext context)` 這個方法,這個方法又會呼叫`initApplicationContext()` 方法 ```java @Override protected void initApplicationContext() { super.initApplicationContext(); if (getViewClass() == null) { throw new IllegalArgumentException("Property 'viewClass' is required"); } } ``` 在子類中會看到 `viewClass` 屬性一般會在構造中法中設定 #### getCacheKey 重寫 `getCacheKey(String viewName, Locale locale)` 方法,忽略 `locale` 引數,僅僅使用 `viewName` 作為快取 KEY,如下: ```java @Override protected Object getCacheKey(String viewName, Locale locale) { // 重寫了父類的方法,去除locale直接返回viewName return viewName; } ``` 也就是說,不支援 Locale 特性 #### canHandle `canHandle(String viewName, Locale locale)` 方法,判斷傳入的檢視名是否可以被處理,如下: ```java protected boolean canHandle(String viewName, Locale locale) { String[] viewNames = getViewNames(); return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName)); } @Nullable protected String[] getViewNames() { return this.viewNames; } ``` 一般情況下,`viewNames` 指定的檢視名們為空,所以會滿足 viewNames == null 程式碼塊。也就說,所有檢視名都可以被處理 #### applyLifecycleMethods `applyLifecycleMethods(String viewName, AbstractUrlBasedView view)` 方法,程式碼如下: ```java protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) { // 情況一,如果 viewName 有對應的 View Bean 物件,則使用它 ApplicationContext context = getApplicationContext(); if (context != null) { Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName); if (initialized instanceof View) { return (View) initialized; } } // 情況二,直接返回 view return view; } ``` #### createView 重寫 `createView(String viewName, Locale locale)` 方法,增加了對 REDIRECT、FORWARD 的情況的處理,如下: ```java @Override protected View createView(String viewName, Locale locale) throws Exception { // If this resolver is not supposed to handle the given view, // return null to pass on to the next resolver in the chain. // 是否能處理該檢視名稱 if (!canHandle(viewName, locale)) { return null; } // Check for special "redirect:" prefix. if (viewName.startsWith(REDIRECT_URL_PREFIX)) { // 如果是 REDIRECT 開頭,建立 RedirectView 檢視 String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); String[] hosts = getRedirectHosts(); if (hosts != null) { // 設定 RedirectView 物件的 hosts 屬性 view.setHosts(hosts); } // 應用 return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); } // Check for special "forward:" prefix. if (viewName.startsWith(FORWARD_URL_PREFIX)) { // 如果是 FORWARD 開頭,建立 InternalResourceView 檢視 String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); InternalResourceView view = new InternalResourceView(forwardUrl); // 應用 return applyLifecycleMethods(FORWARD_URL_PREFIX, view); } // Else fall back to superclass implementation: calling loadView. // 建立檢視名對應的 View 物件 return super.createView(viewName, locale); } ``` #### loadView 實現 `loadView(String viewName, Locale locale)` 方法,載入 viewName 對應的 View 物件,方法如下: ```java @Override protected View loadView(String viewName, Locale locale) throws Exception { // 建立 viewName 對應的 View 物件 AbstractUrlBasedView view = buildView(viewName); // 應用 View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null); } ``` 其中,`` 處,呼叫 `buildView(String viewName)` 方法,建立 `viewName` 對應的 View 物件,方法如下: ```java protected AbstractUrlBasedView buildView(String viewName) throws Exception { Class viewClass = getViewClass(); Assert.state(viewClass != null, "No view class"); // 建立 AbstractUrlBasedView 物件 AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); // 設定各種屬性 view.setUrl(getPrefix() + viewName + getSuffix()); String contentType = getContentType(); if (contentType != null) { view.setContentType(contentType); } view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); Boolean exposePathVariables = getExposePathVariables(); if (exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); if (exposeContextBeansAsAttributes != null) { view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); } String[] exposedContextBeanNames = getExposedContextBeanNames(); if (exposedContextBeanNames != null) { view.setExposedContextBeanNames(exposedContextBeanNames); } return view; } ``` #### requiredViewClass `requiredViewClass()` 方法,定義了產生的檢視,程式碼如下: ```java protected Class requiredViewClass() { return AbstractUrlBasedView.class; } ``` ### InternalResourceViewResolver `org.springframework.web.servlet.view.InternalResourceViewResolver`,繼承 UrlBasedViewResolver 類,解析出 JSP 的 ViewResolver 實現類 #### 構造方法 ```java public class InternalResourceViewResolver extends UrlBasedViewResolver { /** * 判斷 javax.servlet.jsp.jstl.core.Config 是否存在 */ private static final boolean jstlPresent = ClassUtils.isPresent( "javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader()); @Nullable private Boolean alwaysInclude; public InternalResourceViewResolver() { // 獲得 viewClass Class viewClass = requiredViewClass(); if (InternalResourceView.class == viewClass && jstlPresent) { viewClass = JstlView.class; } // 設定 viewClass setViewClass(viewClass); } } ``` 從構造方法中,可以看出,檢視名會是 InternalResourceView 或 JstlView 類。