新聞中心
維護老項目的時候,我們總會遇到一些奇奇怪怪的需求,解決這些奇葩問題可能才是我們開發(fā)的常態(tài)。

在夏河等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站設(shè)計、成都網(wǎng)站制作 網(wǎng)站設(shè)計制作按需定制,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),品牌網(wǎng)站建設(shè),成都營銷網(wǎng)站建設(shè),外貿(mào)營銷網(wǎng)站建設(shè),夏河網(wǎng)站建設(shè)費用合理。
這不,最近就有小伙伴問了這樣一個問題:
這個小伙伴想在 Spring Boot 中同時使用多個視圖解析器,一般來說我們正常設(shè)計一個項目時,肯定不會搞成這樣,要么前后端分離不需要視圖解析器,要么前后端不分需要視圖解析器,但是即使需要一般也只會使用一種視圖解析器,而不會多種視圖解析器混在一起使用。
不過現(xiàn)在既然小伙伴提出了這個問題,我們就來看看這個需求能不能做!先說結(jié)論:技術(shù)上來說這個當(dāng)然是可以實現(xiàn)的,而且實現(xiàn)方式不難。
不過要把這個問題理解透徹,這就涉及到到 SpringMVC 的工作原理了,今天松哥就來和大家把這個問題稍微梳理下。
初始化方法
在 SpringMVC 中我們可以配置多個視圖解析器,這些視圖解析器最終會在 DispatcherServlet#initViewResolvers 方法中完成加載,如下:
- private void initViewResolvers(ApplicationContext context) {
- this.viewResolvers = null;
- 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);
- }
- }
- 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.
- if (this.viewResolvers == null) {
- this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);
- }
- }
這段代碼的邏輯很清楚:
- 首先將 viewResolvers 變量置空,這個變量將存儲所有的視圖解析器。
- 接下來根據(jù) detectAllViewResolvers 的變量值來決定是否要加載所有的視圖解析器,該變量默認為 true,表示加載所有的視圖解析器,加載所有的視圖解析器就是去 Spring 容器中查找到所有的 ViewResolver 實例,然后給這些 ViewResolver 實例按照 Order 優(yōu)先級進行排序。如果 detectAllViewResolvers 的變量值為 false,表示只加載名為 viewResolver 的視圖解析器。
- 經(jīng)過前面的步驟,如果 viewResolvers 還是為 null,表示用戶壓根就沒有配置視圖解析器,此時調(diào)用 getDefaultStrategies 方法加載一個默認的視圖解析器,以確保我們的系統(tǒng)中至少有一個視圖解析器。
一般來說,在一個 SSM 項目中,如果我們在 SpringMVC 的配置文件中,沒有做任何關(guān)于視圖解析器的配置,那么就會走入第三步。
initViewResolvers 方法的主要目的就是初始化視圖解析器,并對視圖解析器進行排序。從這里我們也可以大概看出來 SpringMVC 中是支持多個視圖解析器同時存在的。
原理分析
上面是視圖解析器的初始化過程。
接下來我們來看看視圖解析器具體是如何發(fā)揮作用的。
小伙伴們知道,一個請求進入 DispatcherServlet 之后,執(zhí)行的方法流程依次是 service->processRequest->doService->doDispatch->processDispatchResult->render->resolveViewName->...
進入 render 方法就差不多進入正題了,我們的頁面渲染將在這個方法中完成。render 方法中包含如下一段代碼:
- View view;
- String viewName = mv.getViewName();
- if (viewName != null) {
- // We need to resolve the view name.
- view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
- if (view == null) {
- throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
- "' in servlet with name '" + getServletName() + "'");
- }
- }
- else {
- // No need to lookup: the ModelAndView object contains the actual View object.
- view = mv.getView();
- if (view == null) {
- throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
- "View object in servlet with name '" + getServletName() + "'");
- }
- }
可以看到,這里獲取到視圖的名字之后,接下來調(diào)用 resolveViewName 方法去獲取一個具體的視圖。在 resolveViewName 方法中,將根據(jù)視圖名稱以及現(xiàn)有的視圖解析器找到對應(yīng)的視圖。
那么這里就存在一個問題,現(xiàn)有的視圖解析器如果有多個,究竟該以哪個為準(zhǔn)呢?
我們來看下 resolveViewName 方法中的執(zhí)行邏輯。
- protected View resolveViewName(String viewName, @Nullable Map
model, - Locale locale, HttpServletRequest request) throws Exception {
- if (this.viewResolvers != null) {
- for (ViewResolver viewResolver : this.viewResolvers) {
- View view = viewResolver.resolveViewName(viewName, locale);
- if (view != null) {
- return view;
- }
- }
- }
- return null;
- }
可以看到,這里就是遍歷所有的 ViewResolver,調(diào)用其 resolveViewName 方法去找到對應(yīng)的 View,找到后就返回了。
ViewResolver 就是我們常說的視圖解析器,我們用 JSP、Thymeleaf、Freemarker 等,都有對應(yīng)的視圖解析器,從下面一張圖中就可以看出 ViewResolver 的繼承類:
不過在 Spring Boot 中,我們并不會直接使用這些視圖解析器,而是使用一個名為 ContentNegotiatingViewResolver 的視圖解析器,這個是 Spring3.0 中引入的的視圖解析器,它不負責(zé)具體的視圖解析,而是根據(jù)當(dāng)前請求的 MIME 類型,從上下文中選擇一個合適的視圖解析器,并將請求工作委托給它。
所以這里我們就先來看看 ContentNegotiatingViewResolver#resolveViewName 方法:
- public View resolveViewName(String viewName, Locale locale) throws Exception {
- RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
- List
requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); - if (requestedMediaTypes != null) {
- List
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;
- }
- }
這里的代碼邏輯也比較簡單:
- 首先是獲取到當(dāng)前的請求對象,可以直接從 RequestContextHolder 中獲取。然后從當(dāng)前請求對象中提取出 MediaType。
- 如果 MediaType 不為 null,則根據(jù) MediaType,找到合適的視圖解析器,并將解析出來的 View 返回。
- 如果 MediaType 為 null,則為兩種情況,如果 useNotAcceptableStatusCode 為 true,則返回 NOT_ACCEPTABLE_VIEW 視圖,這個視圖其實是一個 406 響應(yīng),表示客戶端錯誤,服務(wù)器端無法提供與 Accept-Charset 以及 Accept-Language 消息頭指定的值相匹配的響應(yīng);如果 useNotAcceptableStatusCode 為 false,則返回 null。
現(xiàn)在問題的核心其實就變成 getCandidateViews 方法和 getBestView 方法了,看名字就知道,前者是獲取所有的候選 View,后者則是從這些候選 View 中選擇一個最佳的 View,我們一個一個來看。
先來看 getCandidateViews:
- private List
getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) - throws Exception {
- List
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
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 分為兩個步驟:
- 調(diào)用各個 ViewResolver 中的 resolveViewName 方法去加載出對應(yīng)的 View 對象。
- 根據(jù) MediaType 提取出擴展名,再根據(jù)擴展名去加載 View 對象,在實際應(yīng)用中,這一步我們都很少去配置,所以一步基本上是加載不出來 View 對象的,主要靠第一步。
第一步去加載 View 對象,其實就是根據(jù)你的 viewName,再結(jié)合 ViewResolver 中配置的 prefix、suffix、templateLocation 等屬性,找到對應(yīng)的 View,方法執(zhí)行流程依次是 resolveViewName->createView->loadView。
具體執(zhí)行的方法我就不一一貼出來了,唯一需要說的一個重點就是最后的 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 加載出來后,會調(diào)用其 checkResource 方法判斷 View 是否存在,如果存在就返回 View,不存在就返回 null。
這是一個非常關(guān)鍵的步驟,但是我們常用的視圖對此的處理卻不盡相同:
- FreeMarkerView:會老老實實檢查。
- ThymeleafView:沒有檢查這個環(huán)節(jié)(Thymeleaf 的整個 View 體系不同于 FreeMarkerView 和 JstlView)。
- JstlView:檢查結(jié)果總是返回 true。
至此,我們就找到了所有的候選 View,但是大家需要注意,這個候選 View 不一定存在,在有 Thymeleaf 的情況下,返回的候選 View 不一定可用,在 JstlView 中,候選 View 也不一定真的存在。
接下來調(diào)用 getBestView 方法,從所有的候選 View 中找到最佳的 View。getBestView 方法的邏輯比較簡單,就是查找看所有 View 的 MediaType,然后和請求的 MediaType 數(shù)組進行匹配,第一個匹配上的就是最佳 View,這個過程它不會檢查視圖是否真的存在,所以就有可能選出來一個壓根沒有的視圖,最終導(dǎo)致 404。
這就是整個 View 的加載過程。
具體應(yīng)用
如果是單個視圖,這套加載流程沒什么問題,但是如果是多個視圖解析器同時存在,就可能會有問題。
松哥一個一個來說明。
第一種情況:
FreeMarkerView、ThymeleafView 以及 JstlView 在項目中只存在任意一個,這種情況沒任何問題,這也是小伙伴們?nèi)粘3R姷氖褂脠鼍啊?/p>
第二種情況:
FreeMarkerView+ThymeleafView 組合。如果項目中同時存在這兩種視圖解析器,由于 FreeMarkerView 會老老實實檢查視圖是否存在,而 ThymeleafView 不會檢查,所以需要確保 FreeMarkerViewResolver 的優(yōu)先級高于 ThymeleafViewResolver 的優(yōu)先級。這樣就能夠確保視圖加載的時候先去加載 FreeMarkerView(FreeMarkerView 如果不存在,則不會列為候選 View),再去加載 ThymeleafView,這樣無論是 FreeMarkerView 還是 ThymeleafView,都能夠正常加載到(回顧前面所講 getBestView 方法邏輯)。假如 ThymeleafViewResolver 的優(yōu)先級高于 FreeMarkerViewResolver,那么就會出現(xiàn)如下情況:用戶請求一個 Freemarker 視圖,結(jié)果在 getCandidateViews 方法中返回了兩個視圖,依次是 ThymeleafView 和 FreeMarkerView,但是實際上 ThymeleafView 中的視圖是不存在的,結(jié)果在 getBestView 方法中,按順序直接匹配到 ThymeleafView,最終導(dǎo)致運行出錯。
在 Spring Boot 中,如果我們引入了 Freemarker 和 Thyemeleaf 的 starter,默認情況下,F(xiàn)reemarker 和 Thymeleaf 的優(yōu)先級相同,都是 Ordered.LOWEST_PRECEDENCE - 5,但是由于 Freemarker 總是被優(yōu)先加載,而排序時由于兩者優(yōu)先級相同所以位置不變,所以在具體代碼實踐中,F(xiàn)reeMarkerViewResolver 總是排在 ThymeleafViewResolver 前面,F(xiàn)reeMarkerView 會自動檢查視圖是否存在,所以這樣的排序剛剛恰到好處。在具體代碼實踐中,如果我們在項目中同時引入了 Freemarker 和 Thymeleaf,可以不用做任何配置直接同時使用這兩種視圖解析器。
這里要吐槽一下,網(wǎng)上看多人說默認情況下 Freemarker 優(yōu)先級高于 Thymeleaf,不知道誰抄誰的,反正都說錯了,還是要嚴謹呀!
第三種情況:
Freemarker+Jsp 組合,如果項目中同時使用了這兩種視圖解析器,則只需要對 jsp 進行常規(guī)配置即可,不需要額外配置。所謂的常規(guī)配置就是首先引入所需依賴:
org.apache.tomcat.embed tomcat-embed-jasper provided javax.servlet jstl
然后配置一下 jsp 視圖的前綴后綴啥的:
- @Configuration
- public class WebConfig implements WebMvcConfigurer {
- @Override
- public void configureViewResolvers(ViewResolverRegistry registry) {
- registry.jsp("/", ".jsp");
- }
- }
這就可以了。
為什么這個組合這么簡單呢?原因如下:
在 Spring 設(shè)計中,InternalResourceView 其實就是兜底的,所以它不會檢查視圖是否真的存在,它的優(yōu)先級也是最低的。
由于 InternalResourceView 的優(yōu)先級最低,排在 Freemarker 后面,而 Freemarker 會自動檢查視圖是否存在,所以對于這個組合我們不需要額外配置。
第四種情況:
Thymeleaf+Jsp 組合。這個組合稍微有點麻煩,因為 Thymeleaf 和 InternalResourceView 都不會去檢查視圖是否存在,而 Thymeleaf 的優(yōu)先級高于 Jsp,所以 Thymeleaf 會“吞掉” Jsp 視圖的請求。
想要這兩個視圖解析器同時存在,必須要有一個視圖解析器具備檢查視圖是否存在的能力。Jsp 在這塊的配置相對容易一些,所以我們選擇對 InternalResourceView 做一些定制。
具體辦法如下,首先定義類繼承自 InternalResourceView 并重寫 checkResource 方法:
- public class HandleResourceViewExists extends InternalResourceView {
- @Override
- public boolean checkResource(Locale locale) {
- File file = new File(this.getServletContext().getRealPath("/") + getUrl());
- //判斷頁面是否存在
- return file.exists();
- }
- }
InternalResourceView 默認的 checkResource 方法總是返回 true,現(xiàn)在我們稍微修改一下,讓它去判斷一下視圖文件是否存在,如果存在,返回 true,否則返回 false。
配置完成后,將新的 HandleResourceViewExists 重新配置,同時修改優(yōu)先級,使之優(yōu)先級大于 ThymeleafViewResolver,如下:
- @Configuration
- public class WebConfig implements WebMvcConfigurer {
- @Override
- public void configureViewResolvers(ViewResolverRegistry registry) {
- registry.jsp("/", ".jsp").viewClass(HandleResourceViewExists.class);
- registry.order(1);
- }
- }
如此之后,這兩個視圖解析器就可以同時存在了。
第五種情況:
Freemarker+Thymeleaf+Jsp,看了前面四種,第五種情況應(yīng)該就不用我多說了吧~
好啦,這個問題從原理到應(yīng)用,都給大伙捋了一遍了,感興趣的小伙伴趕緊試試哦~
本文轉(zhuǎn)載自微信公眾號「江南一點雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系江南一點雨公眾號。
網(wǎng)站題目:這得多老的項目才會有這么奇葩的需求
本文路徑:http://www.5511xx.com/article/cddspej.html


咨詢
建站咨詢
