SpringMVC 九大元件之 ViewResolver 深入分析
SpringMVC 中的九大元件前面已經和大家分享了好幾個了,今天我們來繼續檢視解析器的分析。
關於檢視解析器,松哥其實在之前的文章中有和大家分享過,那一次是為了解決多個檢視共存的問題
ViewResolver 其實就是我們心心念唸的檢視解析器,用過 SpringMVC 的小夥伴都知道 SpringMVC 中有一個檢視解析器,今天我們就來分析一下這個檢視解析器到底是怎麼工作的。
1.概覽
首先我們來大概看一下 ViewResolver 介面是什麼樣子的:
public interface ViewResolver {
@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;
}
這個介面中只有一個方法,可以看到,非常簡單,就是透過檢視名和 Locale,找到對應的 View 返回即可。
如圖直接繼承自 ViewResolver 介面的類有四個,作用如下:
- ContentNegotiatingViewResolver:支援 MediaType 和字尾的檢視解析器。
- BeanNameViewResolver:這個是直接根據檢視名去 Spring 容器中查詢相應的 Bean 並返回。
- AbstractCachingViewResolver:具有快取功能的檢視解析器。
- ViewResolverComposite:這是一個組合的檢視解析器,屆時可以用來代理其他具體幹活的檢視解析器。
接下來我們就對這四個檢視解析器逐一進行介紹,先從最簡單的 BeanNameViewResolver 開始吧。
2.BeanNameViewResolver
BeanNameViewResolver 的處理方式非常簡單粗暴,直接根據 viewName 去 Spring 容器中查詢相應的 Bean 並返回,如下:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
if (!context.containsBean(viewName)) {
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
return null;
}
return context.getBean(viewName, View.class);
}
先去判斷下有沒有相應的 Bean,然後再檢查下 Bean 的型別對不對,都沒問題,直接查詢返回即可。
3.ContentNegotiatingViewResolver
ContentNegotiatingViewResolver 其實是目前廣泛使用的一個檢視解析器,主要是新增了對 MediaType 的支援。ContentNegotiatingViewResolver 這個是 Spring3.0 中引入的的檢視解析器,它不負責具體的檢視解析,而是根據當前請求的 MIME 型別,從上下文中選擇一個合適的檢視解析器,並將請求工作委託給它。
這裡我們就先來看看 ContentNegotiatingViewResolver#resolveViewName 方法:
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
ListMediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
ListView> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
if (this.useNotAcceptableStatusCode) {
return NOT_ACCEPTABLE_VIEW;
}
else {
return null;
}
}
這裡的程式碼邏輯也比較簡單:
- 首先是獲取到當前的請求物件,可以直接從 RequestContextHolder 中獲取。然後從當前請求物件中提取出 MediaType。
- 如果 MediaType 不為 null,則根據 MediaType,找到合適的檢視解析器,並將解析出來的 View 返回。
- 如果 MediaType 為 null,則為兩種情況,如果 useNotAcceptableStatusCode 為 true,則返回 NOT_ACCEPTABLE_VIEW 檢視,這個檢視其實是一個 406 響應,表示客戶端錯誤,伺服器端無法提供與 Accept-Charset 以及 Accept-Language 訊息頭指定的值相匹配的響應;如果 useNotAcceptableStatusCode 為 false,則返回 null。
現在問題的核心其實就變成 getCandidateViews 方法和 getBestView 方法了,看名字就知道,前者是獲取所有的候選 View,後者則是從這些候選 View 中選擇一個最佳的 View,我們一個一個來看。
先來看 getCandidateViews:
private ListView> getCandidateViews(String viewName, Locale locale, ListMediaType> requestedMediaTypes)
throws Exception {
ListView> candidateViews = new ArrayList>();
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
for (MediaType requestedMediaType : requestedMediaTypes) {
ListString> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
獲取所有的候選 View 分為兩個步驟:
- 呼叫各個 ViewResolver 中的 resolveViewName 方法去載入出對應的 View 物件。
- 根據 MediaType 提取出副檔名,再根據副檔名去載入 View 物件,在實際應用中,這一步我們都很少去配置,所以一步基本上是載入不出來 View 物件的,主要靠第一步。
第一步去載入 View 物件,其實就是根據你的 viewName,再結合 ViewResolver 中配置的 prefix、suffix、templateLocation 等屬性,找到對應的 View,方法執行流程依次是 resolveViewName->createView->loadView。
具體執行的方法我就不一一貼出來了,唯一需要說的一個重點就是最後的 loadView 方法,我們來看下這個方法:
protected View loadView(String viewName, Locale locale) throws Exception {
AbstractUrlBasedView view = buildView(viewName);
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}
在這個方法中,View 載入出來後,會呼叫其 checkResource 方法判斷 View 是否存在,如果存在就返回 View,不存在就返回 null。
這是一個非常關鍵的步驟,但是我們常用的檢視對此的處理卻不盡相同:
- FreeMarkerView:會老老實實檢查。
- ThymeleafView:沒有檢查這個環節(Thymeleaf 的整個 View 體系不同於 FreeMarkerView 和 JstlView)。
- JstlView:檢查結果總是返回 true。
至此,我們就找到了所有的候選 View,但是大家需要注意,這個候選 View 不一定存在,在有 Thymeleaf 的情況下,返回的候選 View 不一定可用,在 JstlView 中,候選 View 也不一定真的存在。
接下來呼叫 getBestView 方法,從所有的候選 View 中找到最佳的 View。getBestView 方法的邏輯比較簡單,就是查詢看所有 View 的 MediaType,然後和請求的 MediaType 陣列進行匹配,第一個匹配上的就是最佳 View,這個過程它不會檢查檢視是否真的存在,所以就有可能選出來一個壓根沒有的檢視,最終導致 404。
這就是 ContentNegotiatingViewResolver#resolveViewName 方法的工作過程。
那麼這裡還涉及到一個問題,ContentNegotiatingViewResolver 中的 ViewResolver 是從哪裡來的?這個有兩種來源:預設的和手動配置的。我們來看如下一段初始化程式碼:
@Override
protected void initServletContext(ServletContext servletContext) {
CollectionViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
else {
for (int i = 0; i this.viewResolvers.size(); i++) {
ViewResolver vr = this.viewResolvers.get(i);
if (matchingBeans.contains(vr)) {
continue;
}
String name = vr.getClass().getName() + i;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
- 首先獲取到 matchingBeans,這個是獲取到了 Spring 容器中的所有檢視解析器。
- 如果 viewResolvers 變數為 null,也就是開發者沒有給 ContentNegotiatingViewResolver 配置檢視解析器,此時會把查到的 matchingBeans 賦值給 viewResolvers。
- 如果開發者為 ContentNegotiatingViewResolver 配置了相關的檢視解析器,則去檢查這些檢視解析器是否存在於 matchingBeans 中,如果不存在,則進行初始化操作。
這就是 ContentNegotiatingViewResolver 所做的事情。
4.AbstractCachingViewResolver
檢視這種檔案有一個特點,就是一旦開發好了不怎麼變,所以將之快取起來提高載入速度就顯得尤為重要了。事實上我們使用的大部分檢視解析器都是支援快取功能,也即 AbstractCachingViewResolver 實際上有很多用武之地。
我們先來大致瞭解一下 AbstractCachingViewResolver,然後再來學習它的子類。
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
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);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
- 首先如果沒有開啟快取,則直接呼叫 createView 方法建立檢視返回。
- 呼叫 getCacheKey 方法獲取快取的 key。
- 去 viewAccessCache 中查詢快取 View,找到了就直接返回。
- 去 viewCreationCache 中查詢快取 View,找到了就直接返回,沒找到就呼叫 createView 方法建立新的 View,並將 View 放到兩個快取池中。
- 這裡有兩個快取池,兩個快取池的區別在於,viewAccessCache 的型別是 ConcurrentHashMap,而 viewCreationCache 的型別是 LinkedHashMap。前者支援併發訪問,效率非常高;後者則限制了快取最大數,效率低於前者。當後者快取數量達到上限時,會自動刪除它裡邊的元素,在刪除自身元素的過程中,也會刪除前者 viewAccessCache 中對應的元素。
那麼這裡還涉及到一個方法,那就是 createView,我們也來稍微看一下:
@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;
可以看到,createView 中呼叫了 loadView,而 loadView 則是一個抽象方法,具體的實現要去子類中檢視了。
這就是快取 View 的查詢過程。
直接繼承 AbstractCachingViewResolver 的檢視解析器有四種:ResourceBundleViewResolver、XmlViewResolver、UrlBasedViewResolver 以及 ThymeleafViewResolver,其中前兩種從 Spring5.3 開始就已經被廢棄掉了,因此這裡松哥就不做過多介紹,我們主要來看下後兩者。
4.1 UrlBasedViewResolver
UrlBasedViewResolver 重寫了父類的 getCacheKey、createView、loadView 三個方法:
getCacheKey
@Override
protected Object getCacheKey(String viewName, Locale locale) {
return viewName;
}
父類的 getCacheKey 是 viewName + '_' + locale
,現在變成了 viewName。
createView
@Override
protected View createView(String viewName, Locale locale) throws Exception {
if (!canHandle(viewName, locale)) {
return null;
}
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}
return super.createView(viewName, locale);
}
- 首先呼叫 canHandle 方法判斷是否支援這裡的邏輯檢視。
- 接下來判斷邏輯檢視名字首是不是
redirect:
,如果是,則表示這是一個重定向檢視,則構造 RedirectView 進行處理。 - 接下來判斷邏輯檢視名字首是不是
forward:
,如果是,則表示這是一個服務端跳轉,則構造 InternalResourceView 進行處理。 - 如果前面都不是,則呼叫父類的 createView 方法去構建檢視,這最終會呼叫到子類的 loadView 方法。
loadView
@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);
}
這裡邊就幹了三件事:
- 呼叫 buildView 方法構建 View。
- 呼叫 applyLifecycleMethods 方法完成 View 的初始化。
- 檢車 View 是否存在並返回。
第三步比較簡單,沒啥好說的,主要就是檢查檢視檔案是否存在,像我們常用的 Jsp 檢視解析器以及 Freemarker 檢視解析器都會去檢查,但是 Thymeleaf 不會去檢查。這裡主要是前兩步,松哥要和大家著重說一下,這裡又涉及到兩個方法 buildView 和 applyLifecycleMethods。
4.1.1 buildView
這個方法就是用來構建檢視的:
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
AbstractUrlBasedView view = instantiateView();
view.setUrl(getPrefix() + viewName + getSuffix());
view.setAttributesMap(getAttributesMap());
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
String requestContextAttribute = getRequestContextAttribute();
if (requestContextAttribute != null) {
view.setRequestContextAttribute(requestContextAttribute);
}
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;
}
- 首先呼叫 instantiateView 方法,根據我們在配置檢視解析器時提供的 viewClass,構建一個 View 物件返回。
- 給 view 配置 url,就是字首+viewName+字尾,其中字首字尾都是我們在配置檢視解析器的時候提供的。
- 同理,如果使用者在配置檢視解析器時提供了 content-type,也將其設定給 View 物件。
- 配置 requestContext 的屬性名稱。
- 配置 exposePathVariables,也就是透過
@PathVaribale
註解標記的引數資訊。 - 配置 exposeContextBeansAsAttributes,表示是否可以在 View 中使用容器中的 Bean,該引數我們可以在配置檢視解析器時提供。
- 配置 exposedContextBeanNames,表示可以在 View 中使用容器中的哪些 Bean,該引數我們可以在配置檢視解析器時提供。
就這樣,檢視就構建好了,是不是非常 easy!
4.1.2 applyLifecycleMethods
protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
ApplicationContext context = getApplicationContext();
if (context != null) {
Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
if (initialized instanceof View) {
return (View) initialized;
}
}
return view;
}
這個就是 Bean 的初始化,沒啥好說的。
UrlBasedViewResolver 的子類還是比較多的,其中有兩個比較有代表性的,分別是我們使用 JSP 時所用的 InternalResourceViewResolver 以及當我們使用 Freemarker 時所用的 FreeMarkerViewResolver,由於這兩個我們比較常見,因此松哥在這裡再和大家介紹一下這兩個元件。
4.2 InternalResourceViewResolver
當我們使用 JSP 時,可能會用到這個檢視解析器。
InternalResourceViewResolver 主要乾了 4 件事:
- 透過 requiredViewClass 方法規定了檢視。
@Override
protected Class?> requiredViewClass() {
return InternalResourceView.class;
}
- 在構造方法中呼叫 requiredViewClass 方法去確定檢視,如果專案中引入了 JSTL,則會將檢視調整為 JstlView。
- 重寫了 instantiateView 方法,會根據實際情況初始化不同的 View:
@Override
protected AbstractUrlBasedView instantiateView() {
return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :
(getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));
}
會根據實際情況初始化 InternalResourceView 或者 JstlView,或者呼叫父類的方法完成 View 的初始化。
- buildView 方法也重寫了,如下:
@Override
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
InternalResourceView view = (InternalResourceView) super.buildView(viewName);
if (this.alwaysInclude != null) {
view.setAlwaysInclude(this.alwaysInclude);
}
view.setPreventDispatchLoop(true);
return view;
}
這裡首先呼叫父類方法構建出 InternalResourceView,然後配置 alwaysInclude,表示是否允許在使用 forward 的情況下也允許使用 include,最後面的 setPreventDispatchLoop 方法則是防止迴圈呼叫。
4.3 FreeMarkerViewResolver
FreeMarkerViewResolver 和 UrlBasedViewResolver 之間還隔了一個 AbstractTemplateViewResolver,AbstractTemplateViewResolver 比較簡單,裡邊只是多出來了五個屬性而已,這五個屬性松哥在之前和大家分享 Freemarker 用法的時候都已經說過了,這裡再和大家囉嗦下:
- exposeRequestAttributes:是否將 RequestAttributes 暴露給 View 使用。
- allowRequestOverride:當 RequestAttributes 和 Model 中的資料同名時,是否允許 RequestAttributes 中的引數覆蓋 Model 中的同名引數。
- exposeSessionAttributes:是否將 SessionAttributes 暴露給 View 使用。
- allowSessionOverride:當 SessionAttributes 和 Model 中的資料同名時,是否允許 SessionAttributes 中的引數覆蓋 Model 中的同名引數。
- exposeSpringMacroHelpers:是否將 RequestContext 暴露出來供 Spring Macro 使用。
這就是 AbstractTemplateViewResolver 特性,比較簡單,再來看 FreeMarkerViewResolver。
public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {
public FreeMarkerViewResolver() {
setViewClass(requiredViewClass());
}
public FreeMarkerViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
@Override
protected Class?> requiredViewClass() {
return FreeMarkerView.class;
}
@Override
protected AbstractUrlBasedView instantiateView() {
return (getViewClass() == FreeMarkerView.class ? new FreeMarkerView() : super.instantiateView());
}
}
FreeMarkerViewResolver 的原始碼就很簡單了,配置一下前字尾、重寫 requiredViewClass 方法提供 FreeMarkerView,重寫 instantiateView 方法完成 View 的初始化。
ThymeleafViewResolver 繼承自 AbstractCachingViewResolver,具體的工作流程和前面的差不多,因此這裡也就不做過多介紹了。需要注意的是,ThymeleafViewResolver#loadView 方法並不會去檢查檢視模版是否存在,所以有可能會最終會返回一個不存在的檢視。
5.ViewResolverComposite
最後我們再來看下 ViewResolverComposite,ViewResolverComposite 其實我們在前面的原始碼分析中已經多次見到過這種模式了,透過 ViewResolverComposite 來代理其他的 ViewResolver,不同的是,這裡的 ViewResolverComposite 還為其他 ViewResolver 做了一些初始化操作。為對應的 ViewResolver 分別配置了 applicationContext 以及 servletContext。這裡的程式碼比較簡單,我就不貼出來了,最後在 ViewResolverComposite#resolveViewName 方法中,遍歷其他檢視解析器進行處理:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
6.小結
好啦,今天主要和小夥伴們聊了下 SpringMVC 中檢視解析器的工作流程,結合松哥之前的文章[SpringMVC 中如何同時存在多個檢視解析器],相信大家對於 SpringMVC 中的檢視解析器的理解會更進一步。
好啦,今天就先和大家聊這麼多~
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4369/viewspace-2807322/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- SpringMVC之原始碼分析--ViewResolver(五)SpringMVC原始碼View
- SpringMVC重要介面ViewResolverSpringMVCView
- 精盡Spring MVC原始碼分析 - ViewResolver 元件SpringMVC原始碼View元件
- SpringMVC 框架系列之元件概述與配置詳解SpringMVC框架元件
- SpringMVC系列之SpringMVC快速入門 MVC設計模式介紹+什麼是SpringMVC+ SpringMVC的作用及其基本使用+元件解析+註解解析SpringMVC設計模式元件
- SpringMVC(一)SpringMVC的處理流程、元件說明、搭建SpringMVC元件
- element ScrollBar滾動元件原始碼深入分析元件原始碼
- “過時”的SpringMVC我們到底在用什麼?深入分析DispatchServlet原始碼SpringMVCServlet原始碼
- SpringMVC之學習(0)SpringMVC
- SpringMVC之檔案上傳SpringMVC
- SpringMVC原始碼系列:九大元件小記SpringMVC原始碼元件
- SpringMVC之ajax非同步互動SpringMVC非同步
- Framework是什麼?Flutter Framework 之 RenderObject深入分析FrameworkFlutterObject
- Yii2 之錯誤處理深入分析
- 初見SpringMVC之資料繫結SpringMVC
- public interface View介面和public interface ViewResolver介面介紹View
- jmeter學習指南之深入分析跨域傳遞cookieJMeter跨域Cookie
- 【SpringMVC】SpringMVC搭建框架SpringMVC框架
- 關於 SAP UI5 Container 控制元件 aggregation 的深入分析試讀版UIAI控制元件
- 深入分析C++物件模型之移動建構函式C++物件模型函式
- 司機帶你深入分析 Laravel 響應之檢視一Laravel
- Flutter 命令本質之 Flutter tools 機制原始碼深入分析Flutter原始碼
- SpringMVC原始碼之引數解析繫結原理SpringMVC原始碼
- SpringMVC之學習(2)值得接收和傳遞SpringMVC
- SpringMVC學習筆記之---簡單入門SpringMVC筆記
- 一起學習SSM框架之SpringMVC(五)SSM框架SpringMVC
- SpringMVC學習系列(10) 之 異常處理SpringMVC
- React之受控元件和非受控元件React元件
- React 之受控元件和非受控元件React元件
- React元件之ClockReact元件
- SpringMVC---IDEA 搭建SpringMVC工程SpringMVCIdea
- SpringMvc - SpringMvc的執行流程SpringMVC
- Python MetaClass深入分析Python
- 深入分析KubernetesCriticalPod(二)
- AQS原始碼深入分析之獨佔模式-ReentrantLock鎖特性詳解AQS原始碼模式ReentrantLock
- 《深入分析JavaWeb技術內幕》之讀書筆記(篇三)JavaWeb筆記
- would dispatch back to the current handler URL [/doLogin] again. Check your ViewResolver setup!AIView
- SpringMVCSpringMVC