您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何理解ViewResolver組件”,在日常操作中,相信很多人在如何理解ViewResolver組件問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解ViewResolver組件”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
首先我們來大概看一下 ViewResolver 接口是什么樣子的:
public interface ViewResolver { @Nullable View resolveViewName(String viewName, Locale locale) throws Exception; }
這個接口中只有一個方法,可以看到,非常簡單,就是通過視圖名和 Locale,找到對應的 View 返回即可。
如圖直接繼承自 ViewResolver 接口的類有四個,作用如下:
ContentNegotiatingViewResolver:支持 MediaType 和后綴的視圖解析器。
BeanNameViewResolver:這個是直接根據視圖名去 Spring 容器中查找相應的 Bean 并返回。
AbstractCachingViewResolver:具有緩存功能的視圖解析器。
ViewResolverComposite:這是一個組合的視圖解析器,屆時可以用來代理其他具體干活的視圖解析器。
接下來我們就對這四個視圖解析器逐一進行介紹,先從最簡單的 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 的類型對不對,都沒問題,直接查找返回即可。
ContentNegotiatingViewResolver 其實是目前廣泛使用的一個視圖解析器,主要是添加了對 MediaType 的支持。ContentNegotiatingViewResolver 這個是 Spring3.0 中引入的的視圖解析器,它不負責具體的視圖解析,而是根據當前請求的 MIME 類型,從上下文中選擇一個合適的視圖解析器,并將請求工作委托給它。
這里我們就先來看看 ContentNegotiatingViewResolver#resolveViewName 方法:
public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); List<mediatype> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); if (requestedMediaTypes != null) { List<view> 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 List<view> getCandidateViews(String viewName, Locale locale, List<mediatype> requestedMediaTypes) throws Exception { List<view> 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) { List<string> 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) { Collection<viewresolver> 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 所做的事情。
視圖這種文件有一個特點,就是一旦開發好了不怎么變,所以將之緩存起來提高加載速度就顯得尤為重要了。事實上我們使用的大部分視圖解析器都是支持緩存功能,也即 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 開始就已經被廢棄掉了,因此這里松哥就不做過多介紹,我們主要來看下后兩者。
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 不會去檢查(具體參見:SpringMVC 中如何同時存在多個視圖解析器一文)。這里主要是前兩步,松哥要和大家著重說一下,這里又涉及到兩個方法 buildView 和 applyLifecycleMethods。
這個方法就是用來構建視圖的:
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!
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,由于這兩個我們比較常見,因此松哥在這里再和大家介紹一下這兩個組件。
當我們使用 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 方法則是防止循環調用。
FreeMarkerViewResolver 和 UrlBasedViewResolver 之間還隔了一個 AbstractTemplateViewResolver,AbstractTemplateViewResolver 比較簡單,里邊只是多出來了五個屬性而已,這五個屬性松哥在之前和大家分享 Freemarker 用法的時候都已經說過了(參見:Spring Boot + 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 方法并不會去檢查視圖模版是否存在,所以有可能會最終會返回一個不存在的視圖(參見:SpringMVC 中如何同時存在多個視圖解析器一文)。
最后我們再來看下 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; }
到此,關于“如何理解ViewResolver組件”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。