日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
這得多老的項(xiàng)目才會(huì)有這么奇葩的需求

維護(hù)老項(xiàng)目的時(shí)候,我們總會(huì)遇到一些奇奇怪怪的需求,解決這些奇葩問(wèn)題可能才是我們開(kāi)發(fā)的常態(tài)。

在夏河等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作 網(wǎng)站設(shè)計(jì)制作按需定制,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),品牌網(wǎng)站建設(shè),成都營(yíng)銷網(wǎng)站建設(shè),外貿(mào)營(yíng)銷網(wǎng)站建設(shè),夏河網(wǎng)站建設(shè)費(fèi)用合理。

這不,最近就有小伙伴問(wèn)了這樣一個(gè)問(wèn)題:

這個(gè)小伙伴想在 Spring Boot 中同時(shí)使用多個(gè)視圖解析器,一般來(lái)說(shuō)我們正常設(shè)計(jì)一個(gè)項(xiàng)目時(shí),肯定不會(huì)搞成這樣,要么前后端分離不需要視圖解析器,要么前后端不分需要視圖解析器,但是即使需要一般也只會(huì)使用一種視圖解析器,而不會(huì)多種視圖解析器混在一起使用。

不過(guò)現(xiàn)在既然小伙伴提出了這個(gè)問(wèn)題,我們就來(lái)看看這個(gè)需求能不能做!先說(shuō)結(jié)論:技術(shù)上來(lái)說(shuō)這個(gè)當(dāng)然是可以實(shí)現(xiàn)的,而且實(shí)現(xiàn)方式不難。

不過(guò)要把這個(gè)問(wèn)題理解透徹,這就涉及到到 SpringMVC 的工作原理了,今天松哥就來(lái)和大家把這個(gè)問(wèn)題稍微梳理下。

初始化方法

在 SpringMVC 中我們可以配置多個(gè)視圖解析器,這些視圖解析器最終會(huì)在 DispatcherServlet#initViewResolvers 方法中完成加載,如下:

 
 
 
 
  1. private void initViewResolvers(ApplicationContext context) { 
  2.  this.viewResolvers = null; 
  3.  if (this.detectAllViewResolvers) { 
  4.   // Find all ViewResolvers in the ApplicationContext, including ancestor contexts. 
  5.   Map matchingBeans = 
  6.     BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); 
  7.   if (!matchingBeans.isEmpty()) { 
  8.    this.viewResolvers = new ArrayList<>(matchingBeans.values()); 
  9.    // We keep ViewResolvers in sorted order. 
  10.    AnnotationAwareOrderComparator.sort(this.viewResolvers); 
  11.   } 
  12.  } 
  13.  else { 
  14.   try { 
  15.    ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); 
  16.    this.viewResolvers = Collections.singletonList(vr); 
  17.   } 
  18.   catch (NoSuchBeanDefinitionException ex) { 
  19.    // Ignore, we'll add a default ViewResolver later. 
  20.   } 
  21.  } 
  22.  // Ensure we have at least one ViewResolver, by registering 
  23.  // a default ViewResolver if no other resolvers are found. 
  24.  if (this.viewResolvers == null) { 
  25.   this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); 
  26.  } 

這段代碼的邏輯很清楚:

  • 首先將 viewResolvers 變量置空,這個(gè)變量將存儲(chǔ)所有的視圖解析器。
  • 接下來(lái)根據(jù) detectAllViewResolvers 的變量值來(lái)決定是否要加載所有的視圖解析器,該變量默認(rèn)為 true,表示加載所有的視圖解析器,加載所有的視圖解析器就是去 Spring 容器中查找到所有的 ViewResolver 實(shí)例,然后給這些 ViewResolver 實(shí)例按照 Order 優(yōu)先級(jí)進(jìn)行排序。如果 detectAllViewResolvers 的變量值為 false,表示只加載名為 viewResolver 的視圖解析器。
  • 經(jīng)過(guò)前面的步驟,如果 viewResolvers 還是為 null,表示用戶壓根就沒(méi)有配置視圖解析器,此時(shí)調(diào)用 getDefaultStrategies 方法加載一個(gè)默認(rèn)的視圖解析器,以確保我們的系統(tǒng)中至少有一個(gè)視圖解析器。

一般來(lái)說(shuō),在一個(gè) SSM 項(xiàng)目中,如果我們?cè)?SpringMVC 的配置文件中,沒(méi)有做任何關(guān)于視圖解析器的配置,那么就會(huì)走入第三步。

initViewResolvers 方法的主要目的就是初始化視圖解析器,并對(duì)視圖解析器進(jìn)行排序。從這里我們也可以大概看出來(lái) SpringMVC 中是支持多個(gè)視圖解析器同時(shí)存在的。

原理分析

上面是視圖解析器的初始化過(guò)程。

接下來(lái)我們來(lái)看看視圖解析器具體是如何發(fā)揮作用的。

小伙伴們知道,一個(gè)請(qǐng)求進(jìn)入 DispatcherServlet 之后,執(zhí)行的方法流程依次是 service->processRequest->doService->doDispatch->processDispatchResult->render->resolveViewName->...

進(jìn)入 render 方法就差不多進(jìn)入正題了,我們的頁(yè)面渲染將在這個(gè)方法中完成。render 方法中包含如下一段代碼:

 
 
 
 
  1. View view; 
  2. String viewName = mv.getViewName(); 
  3. if (viewName != null) { 
  4.  // We need to resolve the view name. 
  5.  view = resolveViewName(viewName, mv.getModelInternal(), locale, request); 
  6.  if (view == null) { 
  7.   throw new ServletException("Could not resolve view with name '" + mv.getViewName() + 
  8.     "' in servlet with name '" + getServletName() + "'"); 
  9.  } 
  10. else { 
  11.  // No need to lookup: the ModelAndView object contains the actual View object. 
  12.  view = mv.getView(); 
  13.  if (view == null) { 
  14.   throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + 
  15.     "View object in servlet with name '" + getServletName() + "'"); 
  16.  } 

可以看到,這里獲取到視圖的名字之后,接下來(lái)調(diào)用 resolveViewName 方法去獲取一個(gè)具體的視圖。在 resolveViewName 方法中,將根據(jù)視圖名稱以及現(xiàn)有的視圖解析器找到對(duì)應(yīng)的視圖。

那么這里就存在一個(gè)問(wèn)題,現(xiàn)有的視圖解析器如果有多個(gè),究竟該以哪個(gè)為準(zhǔn)呢?

我們來(lái)看下 resolveViewName 方法中的執(zhí)行邏輯。

 
 
 
 
  1. protected View resolveViewName(String viewName, @Nullable Map model, 
  2.   Locale locale, HttpServletRequest request) throws Exception { 
  3.  if (this.viewResolvers != null) { 
  4.   for (ViewResolver viewResolver : this.viewResolvers) { 
  5.    View view = viewResolver.resolveViewName(viewName, locale); 
  6.    if (view != null) { 
  7.     return view; 
  8.    } 
  9.   } 
  10.  } 
  11.  return null; 

可以看到,這里就是遍歷所有的 ViewResolver,調(diào)用其 resolveViewName 方法去找到對(duì)應(yīng)的 View,找到后就返回了。

ViewResolver 就是我們常說(shuō)的視圖解析器,我們用 JSP、Thymeleaf、Freemarker 等,都有對(duì)應(yīng)的視圖解析器,從下面一張圖中就可以看出 ViewResolver 的繼承類:

不過(guò)在 Spring Boot 中,我們并不會(huì)直接使用這些視圖解析器,而是使用一個(gè)名為 ContentNegotiatingViewResolver 的視圖解析器,這個(gè)是 Spring3.0 中引入的的視圖解析器,它不負(fù)責(zé)具體的視圖解析,而是根據(jù)當(dāng)前請(qǐng)求的 MIME 類型,從上下文中選擇一個(gè)合適的視圖解析器,并將請(qǐng)求工作委托給它。

所以這里我們就先來(lái)看看 ContentNegotiatingViewResolver#resolveViewName 方法:

 
 
 
 
  1. public View resolveViewName(String viewName, Locale locale) throws Exception { 
  2.  RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); 
  3.  List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); 
  4.  if (requestedMediaTypes != null) { 
  5.   List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); 
  6.   View bestView = getBestView(candidateViews, requestedMediaTypes, attrs); 
  7.   if (bestView != null) { 
  8.    return bestView; 
  9.   } 
  10.  } 
  11.  if (this.useNotAcceptableStatusCode) { 
  12.   return NOT_ACCEPTABLE_VIEW; 
  13.  } 
  14.  else { 
  15.   return null; 
  16.  } 

這里的代碼邏輯也比較簡(jiǎn)單:

  • 首先是獲取到當(dāng)前的請(qǐng)求對(duì)象,可以直接從 RequestContextHolder 中獲取。然后從當(dāng)前請(qǐng)求對(duì)象中提取出 MediaType。
  • 如果 MediaType 不為 null,則根據(jù) MediaType,找到合適的視圖解析器,并將解析出來(lái)的 View 返回。
  • 如果 MediaType 為 null,則為兩種情況,如果 useNotAcceptableStatusCode 為 true,則返回 NOT_ACCEPTABLE_VIEW 視圖,這個(gè)視圖其實(shí)是一個(gè) 406 響應(yīng),表示客戶端錯(cuò)誤,服務(wù)器端無(wú)法提供與 Accept-Charset 以及 Accept-Language 消息頭指定的值相匹配的響應(yīng);如果 useNotAcceptableStatusCode 為 false,則返回 null。

現(xiàn)在問(wèn)題的核心其實(shí)就變成 getCandidateViews 方法和 getBestView 方法了,看名字就知道,前者是獲取所有的候選 View,后者則是從這些候選 View 中選擇一個(gè)最佳的 View,我們一個(gè)一個(gè)來(lái)看。

先來(lái)看 getCandidateViews:

 
 
 
 
  1. private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) 
  2.   throws Exception { 
  3.  List candidateViews = new ArrayList<>(); 
  4.  if (this.viewResolvers != null) { 
  5.   for (ViewResolver viewResolver : this.viewResolvers) { 
  6.    View view = viewResolver.resolveViewName(viewName, locale); 
  7.    if (view != null) { 
  8.     candidateViews.add(view); 
  9.    } 
  10.    for (MediaType requestedMediaType : requestedMediaTypes) { 
  11.     List extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType); 
  12.     for (String extension : extensions) { 
  13.      String viewNameWithExtension = viewName + '.' + extension; 
  14.      view = viewResolver.resolveViewName(viewNameWithExtension, locale); 
  15.      if (view != null) { 
  16.       candidateViews.add(view); 
  17.      } 
  18.     } 
  19.    } 
  20.   } 
  21.  } 
  22.  if (!CollectionUtils.isEmpty(this.defaultViews)) { 
  23.   candidateViews.addAll(this.defaultViews); 
  24.  } 
  25.  return candidateViews; 

獲取所有的候選 View 分為兩個(gè)步驟:

  1. 調(diào)用各個(gè) ViewResolver 中的 resolveViewName 方法去加載出對(duì)應(yīng)的 View 對(duì)象。
  2. 根據(jù) MediaType 提取出擴(kuò)展名,再根據(jù)擴(kuò)展名去加載 View 對(duì)象,在實(shí)際應(yīng)用中,這一步我們都很少去配置,所以一步基本上是加載不出來(lái) View 對(duì)象的,主要靠第一步。

第一步去加載 View 對(duì)象,其實(shí)就是根據(jù)你的 viewName,再結(jié)合 ViewResolver 中配置的 prefix、suffix、templateLocation 等屬性,找到對(duì)應(yīng)的 View,方法執(zhí)行流程依次是 resolveViewName->createView->loadView。

具體執(zhí)行的方法我就不一一貼出來(lái)了,唯一需要說(shuō)的一個(gè)重點(diǎn)就是最后的 loadView 方法,我們來(lái)看下這個(gè)方法:

 
 
 
 
  1. protected View loadView(String viewName, Locale locale) throws Exception { 
  2.  AbstractUrlBasedView view = buildView(viewName); 
  3.  View result = applyLifecycleMethods(viewName, view); 
  4.  return (view.checkResource(locale) ? result : null); 

在這個(gè)方法中,View 加載出來(lái)后,會(huì)調(diào)用其 checkResource 方法判斷 View 是否存在,如果存在就返回 View,不存在就返回 null。

這是一個(gè)非常關(guān)鍵的步驟,但是我們常用的視圖對(duì)此的處理卻不盡相同:

  • FreeMarkerView:會(huì)老老實(shí)實(shí)檢查。
  • ThymeleafView:沒(méi)有檢查這個(gè)環(huán)節(jié)(Thymeleaf 的整個(gè) View 體系不同于 FreeMarkerView 和 JstlView)。
  • JstlView:檢查結(jié)果總是返回 true。

至此,我們就找到了所有的候選 View,但是大家需要注意,這個(gè)候選 View 不一定存在,在有 Thymeleaf 的情況下,返回的候選 View 不一定可用,在 JstlView 中,候選 View 也不一定真的存在。

接下來(lái)調(diào)用 getBestView 方法,從所有的候選 View 中找到最佳的 View。getBestView 方法的邏輯比較簡(jiǎn)單,就是查找看所有 View 的 MediaType,然后和請(qǐng)求的 MediaType 數(shù)組進(jìn)行匹配,第一個(gè)匹配上的就是最佳 View,這個(gè)過(guò)程它不會(huì)檢查視圖是否真的存在,所以就有可能選出來(lái)一個(gè)壓根沒(méi)有的視圖,最終導(dǎo)致 404。

這就是整個(gè) View 的加載過(guò)程。

具體應(yīng)用

如果是單個(gè)視圖,這套加載流程沒(méi)什么問(wèn)題,但是如果是多個(gè)視圖解析器同時(shí)存在,就可能會(huì)有問(wèn)題。

松哥一個(gè)一個(gè)來(lái)說(shuō)明。

第一種情況:

FreeMarkerView、ThymeleafView 以及 JstlView 在項(xiàng)目中只存在任意一個(gè),這種情況沒(méi)任何問(wèn)題,這也是小伙伴們?nèi)粘3R?jiàn)的使用場(chǎng)景。

第二種情況:

FreeMarkerView+ThymeleafView 組合。如果項(xiàng)目中同時(shí)存在這兩種視圖解析器,由于 FreeMarkerView 會(huì)老老實(shí)實(shí)檢查視圖是否存在,而 ThymeleafView 不會(huì)檢查,所以需要確保 FreeMarkerViewResolver 的優(yōu)先級(jí)高于 ThymeleafViewResolver 的優(yōu)先級(jí)。這樣就能夠確保視圖加載的時(shí)候先去加載 FreeMarkerView(FreeMarkerView 如果不存在,則不會(huì)列為候選 View),再去加載 ThymeleafView,這樣無(wú)論是 FreeMarkerView 還是 ThymeleafView,都能夠正常加載到(回顧前面所講 getBestView 方法邏輯)。假如 ThymeleafViewResolver 的優(yōu)先級(jí)高于 FreeMarkerViewResolver,那么就會(huì)出現(xiàn)如下情況:用戶請(qǐng)求一個(gè) Freemarker 視圖,結(jié)果在 getCandidateViews 方法中返回了兩個(gè)視圖,依次是 ThymeleafView 和 FreeMarkerView,但是實(shí)際上 ThymeleafView 中的視圖是不存在的,結(jié)果在 getBestView 方法中,按順序直接匹配到 ThymeleafView,最終導(dǎo)致運(yùn)行出錯(cuò)。

在 Spring Boot 中,如果我們引入了 Freemarker 和 Thyemeleaf 的 starter,默認(rèn)情況下,F(xiàn)reemarker 和 Thymeleaf 的優(yōu)先級(jí)相同,都是 Ordered.LOWEST_PRECEDENCE - 5,但是由于 Freemarker 總是被優(yōu)先加載,而排序時(shí)由于兩者優(yōu)先級(jí)相同所以位置不變,所以在具體代碼實(shí)踐中,F(xiàn)reeMarkerViewResolver 總是排在 ThymeleafViewResolver 前面,F(xiàn)reeMarkerView 會(huì)自動(dòng)檢查視圖是否存在,所以這樣的排序剛剛恰到好處。在具體代碼實(shí)踐中,如果我們?cè)陧?xiàng)目中同時(shí)引入了 Freemarker 和 Thymeleaf,可以不用做任何配置直接同時(shí)使用這兩種視圖解析器。

這里要吐槽一下,網(wǎng)上看多人說(shuō)默認(rèn)情況下 Freemarker 優(yōu)先級(jí)高于 Thymeleaf,不知道誰(shuí)抄誰(shuí)的,反正都說(shuō)錯(cuò)了,還是要嚴(yán)謹(jǐn)呀!

第三種情況:

Freemarker+Jsp 組合,如果項(xiàng)目中同時(shí)使用了這兩種視圖解析器,則只需要對(duì) jsp 進(jìn)行常規(guī)配置即可,不需要額外配置。所謂的常規(guī)配置就是首先引入所需依賴:

 
 
 
 
  1.  
  2.     org.apache.tomcat.embed 
  3.     tomcat-embed-jasper 
  4.     provided 
  5.  
  6.  
  7.     javax.servlet 
  8.     jstl 
  9.  

然后配置一下 jsp 視圖的前綴后綴啥的:

 
 
 
 
  1. @Configuration 
  2. public class WebConfig implements WebMvcConfigurer { 
  3.     @Override 
  4.     public void configureViewResolvers(ViewResolverRegistry registry) { 
  5.         registry.jsp("/", ".jsp"); 
  6.     } 

這就可以了。

為什么這個(gè)組合這么簡(jiǎn)單呢?原因如下:

在 Spring 設(shè)計(jì)中,InternalResourceView 其實(shí)就是兜底的,所以它不會(huì)檢查視圖是否真的存在,它的優(yōu)先級(jí)也是最低的。

由于 InternalResourceView 的優(yōu)先級(jí)最低,排在 Freemarker 后面,而 Freemarker 會(huì)自動(dòng)檢查視圖是否存在,所以對(duì)于這個(gè)組合我們不需要額外配置。

第四種情況:

Thymeleaf+Jsp 組合。這個(gè)組合稍微有點(diǎn)麻煩,因?yàn)?Thymeleaf 和 InternalResourceView 都不會(huì)去檢查視圖是否存在,而 Thymeleaf 的優(yōu)先級(jí)高于 Jsp,所以 Thymeleaf 會(huì)“吞掉” Jsp 視圖的請(qǐng)求。

想要這兩個(gè)視圖解析器同時(shí)存在,必須要有一個(gè)視圖解析器具備檢查視圖是否存在的能力。Jsp 在這塊的配置相對(duì)容易一些,所以我們選擇對(duì) InternalResourceView 做一些定制。

具體辦法如下,首先定義類繼承自 InternalResourceView 并重寫 checkResource 方法:

 
 
 
 
  1. public class HandleResourceViewExists extends InternalResourceView { 
  2.     @Override 
  3.     public boolean checkResource(Locale locale) { 
  4.         File file = new File(this.getServletContext().getRealPath("/") + getUrl()); 
  5.         //判斷頁(yè)面是否存在 
  6.         return file.exists(); 
  7.     } 

InternalResourceView 默認(rèn)的 checkResource 方法總是返回 true,現(xiàn)在我們稍微修改一下,讓它去判斷一下視圖文件是否存在,如果存在,返回 true,否則返回 false。

配置完成后,將新的 HandleResourceViewExists 重新配置,同時(shí)修改優(yōu)先級(jí),使之優(yōu)先級(jí)大于 ThymeleafViewResolver,如下:

 
 
 
 
  1. @Configuration 
  2. public class WebConfig implements WebMvcConfigurer { 
  3.     @Override 
  4.     public void configureViewResolvers(ViewResolverRegistry registry) { 
  5.         registry.jsp("/", ".jsp").viewClass(HandleResourceViewExists.class); 
  6.         registry.order(1); 
  7.     } 

如此之后,這兩個(gè)視圖解析器就可以同時(shí)存在了。

第五種情況:

Freemarker+Thymeleaf+Jsp,看了前面四種,第五種情況應(yīng)該就不用我多說(shuō)了吧~

好啦,這個(gè)問(wèn)題從原理到應(yīng)用,都給大伙捋了一遍了,感興趣的小伙伴趕緊試試哦~

本文轉(zhuǎn)載自微信公眾號(hào)「江南一點(diǎn)雨」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系江南一點(diǎn)雨公眾號(hào)。


網(wǎng)站名稱:這得多老的項(xiàng)目才會(huì)有這么奇葩的需求
文章轉(zhuǎn)載:http://m.5511xx.com/article/cddspej.html