Spring--視圖內(nèi)容協(xié)商(一)

本文是學(xué)習(xí)了小馬哥在慕課網(wǎng)的課程的《Spring Boot 2.0深度實(shí)踐之核心技術(shù)篇》的內(nèi)容結(jié)合自己的需要和理解做的筆記。

所謂的視圖內(nèi)容協(xié)商,就是讓W(xué)eb客戶端根據(jù)不同的請求策略,實(shí)現(xiàn)服務(wù)端響應(yīng)對應(yīng)視圖的內(nèi)容輸出屈呕。 接下來讓我們深入的了解一下到底Spring是如何視圖內(nèi)容協(xié)商的。

核心組件

  • 視圖解析器:ContentNegotiatingViewResolver
  • 內(nèi)容協(xié)商管理器:ContentNegotiationManager
  • 內(nèi)容協(xié)商策略:ContentNegotiationStrategy

源碼解讀前置工作

在我們要理解Spring的視圖內(nèi)容協(xié)調(diào)流程圖之前,我們需要新建一個spring-boot項(xiàng)目下面,然后進(jìn)行必要的配置來啟動視圖內(nèi)容協(xié)商。我們新建一個模塊名為 springboot-restful 之所以起這個名字,是因?yàn)橐晥D內(nèi)容協(xié)商不僅是對客戶端視圖渲染的協(xié)商操作绩聘,也是針對restful形式的內(nèi)容的請求和響應(yīng)的協(xié)商操作沥割。

1.新建Model-- springboot-restful

目錄.PNG

2.pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
    </dependency>

    <!-- Provided -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.其他代碼

/**
 * Spring 攔截器 配置
 */
@Configuration   //配置
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 配置視圖內(nèi)容協(xié)商
     * @param configurer
     */
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true).favorPathExtension(true);

    }


    /**
     * 解決在IDEA下maven多模塊使用spring-boot跳轉(zhuǎn)JSP 404問題
     * @return
     */
    @Bean
    public WebServerFactoryCustomizer<TomcatServletWebServerFactory> customizer() {
        return (factory -> {
            factory.addContextCustomizers(context -> {
                //當(dāng)前webapp路徑
                String relativePath = "springboot-restful/src/main/webapp";
                File docBaseFile = new File(relativePath);
                if(docBaseFile.exists()) {
                    context.setDocBase(new File(relativePath).getAbsolutePath());
                }
            });
        });
    }

}
/**
 * Spring-boot 啟動引導(dǎo)類
 */
@SpringBootApplication
public class SpringBootRestfulBootStrap {


    public static void main(String[] args) {
        SpringApplication.run(SpringBootRestfulBootStrap.class,args);

    }
}

流程圖

流程圖.PNG

源碼解讀

我們可以根據(jù)上面的流程圖來一起閱讀源碼,這樣能讓我們有個初步的理解凿菩。

步驟一

首先我們來看一下Spring-boot是什么時(shí)候開始初始化聲明ContentNegotiationConfigurer机杜。

org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcContentNegotiationManager

在這個方法中,我們可以看到在容器初始化時(shí),Spring-boot的自動裝配就把ContentNegotiationManager裝在到容器了衅谷。

@Bean
@Override
public ContentNegotiationManager mvcContentNegotiationManager() {
   ContentNegotiationManager manager = super.mvcContentNegotiationManager();
   List<ContentNegotiationStrategy> strategies = manager.getStrategies();
   ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator();
   while (iterator.hasNext()) {
      ContentNegotiationStrategy strategy = iterator.next();
      if (strategy instanceof PathExtensionContentNegotiationStrategy) {
         iterator.set(new OptionalPathExtensionContentNegotiationStrategy(
               strategy));
      }
   }
   return manager;
}

super.mvcContentNegotiationManager()源碼:

/**
 * Return a {@link ContentNegotiationManager} instance to use to determine
 * requested {@linkplain MediaType media types} in a given request.
 */
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
   if (this.contentNegotiationManager == null) {
      ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
      configurer.mediaTypes(getDefaultMediaTypes());
      configureContentNegotiation(configurer);
      this.contentNegotiationManager = configurer.buildContentNegotiationManager();
   }
   return this.contentNegotiationManager;
}

在這里通過ContentNegotiationConfigurer來創(chuàng)建ContentNegotiationManager 對象椒拗,我們先來看一下ContentNegotiationConfigurer中都有哪些關(guān)鍵的方法。

public class ContentNegotiationConfigurer {

   private final ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
    .......
            /**
     * Build a {@link ContentNegotiationManager} based on this configurer's settings.
     * @since 4.3.12
     * @see ContentNegotiationManagerFactoryBean#getObject()
     */
    protected ContentNegotiationManager buildContentNegotiationManager() {
        this.factory.addMediaTypes(this.mediaTypes);
        return this.factory.build();
    }

}

在這里 我們可以看到ContentNegotiationConfigurer 類中获黔,聲明了一個ContentNegotiationManagerFactoryBean這也如流程圖中的 步驟1 ---- 關(guān)聯(lián)蚀苛。

步驟二

配置策略 則是使用ContentNegotiationConfigurer 的幾個方法來配置,這里我們在自定義的com.web.configuration.WebMvcConfig中只使用了兩種配置肢执。代碼如下

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorParameter(true).favorPathExtension(true);

}

對應(yīng)的方法代碼如下:

    /**
     * Whether a request parameter ("format" by default) should be used to
     * determine the requested media type. For this option to work you must
     * register {@link #mediaType(String, MediaType) media type mappings}.
     * <p>By default this is set to {@code false}.
     * @see #parameterName(String)
     */
//請求參數(shù)
public ContentNegotiationConfigurer favorParameter(boolean favorParameter) {
   this.factory.setFavorParameter(favorParameter);
   return this;
}
    /**
     * Whether the path extension in the URL path should be used to determine
     * the requested media type.
     * <p>By default this is set to {@code true} in which case a request
     * for {@code /hotels.pdf} will be interpreted as a request for
     * {@code "application/pdf"} regardless of the 'Accept' header.
     */
//URL后綴
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
    this.factory.setFavorPathExtension(favorPathExtension);
    return this;
}

這里就不多做解釋了枉阵,感興趣的小伙伴可以看一下英文注釋,也就輕輕松松明白了预茄。

步驟三/步驟四

添加策略和創(chuàng)建ContentNegotiationManager 放在一起講兴溜。

我們可以從步驟一的源碼中看到 最后是調(diào)用ContentNegotiationConfigurer中的buildContentNegotiationManager()方法來創(chuàng)建ContentNegotiationManager的侦厚。

那么我們來進(jìn)一步看一下代碼。

org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer#buildContentNegotiationManager

protected ContentNegotiationManager buildContentNegotiationManager() {
   this.factory.addMediaTypes(this.mediaTypes);
   return this.factory.build();
}

這個方法在步驟一中我就有貼出拙徽,因?yàn)榉浅V匾俾伲谶@我們可以看到,其實(shí)是使用ContentNegotiationManagerFactoryBean來創(chuàng)建ContentNegotiationManager的膘怕。我們來看一下build()方法想诅。

/**
 * Actually build the {@link ContentNegotiationManager}.
 * @since 5.0
 */
public ContentNegotiationManager build() {
   List<ContentNegotiationStrategy> strategies = new ArrayList<>();

   if (this.strategies != null) {
      strategies.addAll(this.strategies);
   }
   else {
       //是否配置 URL后綴策略
      if (this.favorPathExtension) {
          //聲明并配置 PathExtensionContentNegotiationStrategy 策略
         PathExtensionContentNegotiationStrategy strategy;
         if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
            strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
         }
         else {
            strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
         }
         strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
         if (this.useRegisteredExtensionsOnly != null) {
            strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
         }
         strategies.add(strategy);
      }

       //是否配置了參數(shù)策略
      if (this.favorParameter) {
          //聲明并配置ParameterContentNegotiationStrategy策略
         ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
         strategy.setParameterName(this.parameterName);
         if (this.useRegisteredExtensionsOnly != null) {
            strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
         }
         else {
            strategy.setUseRegisteredExtensionsOnly(true);  // backwards compatibility
         }
         strategies.add(strategy);
      }

      if (!this.ignoreAcceptHeader) {
         strategies.add(new HeaderContentNegotiationStrategy());
      }

      if (this.defaultNegotiationStrategy != null) {
         strategies.add(this.defaultNegotiationStrategy);
      }
   }

   this.contentNegotiationManager = new ContentNegotiationManager(strategies);
   return this.contentNegotiationManager;
}

一下貼出這么多代碼可能有些懵,但是我們對照著流程圖一步一步的看岛心,由于我們已經(jīng)配置了 <u>參數(shù)策略</u>以及<u>URL后綴策略</u>来破。所以 上面的if else 就很好懂了⊥牛看注釋就可以明白了徘禁。

步驟五

關(guān)聯(lián)ContentNegotiatingViewResolver通過org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#viewResolver方法,來初始化Bean ContentNegotiatingViewResolver 髓堪∷椭欤可以看到方法第二行就是關(guān)聯(lián)ContentNegotiationManager的地方。

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
   ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    //關(guān)聯(lián) ContentNegotiationManager
   resolver.setContentNegotiationManager(
         beanFactory.getBean(ContentNegotiationManager.class));
   // ContentNegotiatingViewResolver uses all the other view resolvers to locate
   // a view so it should have a high precedence
   resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
   return resolver;
}

步驟六

ViewResolver Bean 關(guān)聯(lián)

這一步驟非常的繞干旁,看了小馬哥的視頻后驶沼,debugger了很久,不明白是在何時(shí)ContentNegotiatingViewResolver調(diào)用方法關(guān)聯(lián)其他ViewResolver的争群。

org.springframework.web.servlet.view.ContentNegotiatingViewResolver#initServletContext

我們先來看源碼:

@Override
protected void initServletContext(ServletContext servletContext) {
    //獲取到所有 ViewResolvers
   Collection<ViewResolver> matchingBeans =
         BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    //關(guān)聯(lián)他們 
   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);
      }

   }
   if (this.viewResolvers.isEmpty()) {
      logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
            "'viewResolvers' property on the ContentNegotiatingViewResolver");
   }
    //排序
   AnnotationAwareOrderComparator.sort(this.viewResolvers);
   this.cnmFactoryBean.setServletContext(servletContext);
}

從代碼中我們可以很清晰的看到 首先 先獲取到所有的ViewResolver 然后遍歷關(guān)聯(lián)回怜。

但是,到底是什么時(shí)候關(guān)聯(lián)的呢换薄。

經(jīng)過debugger是在

org.springframework.boot.SpringApplication#run(java.lang.String...)啟動方法中的 refreshContext(context);這個調(diào)用后就可以關(guān)聯(lián)上了鹉戚。那么為什么會這樣,我們可以看到

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
      implements ViewResolver, Ordered, InitializingBean {
      ...
      }
    

ContentNegotiatingViewResolver繼承了 WebApplicationObjectSupport 专控。

這個類中有這么一個方法

org.springframework.web.context.support.WebApplicationObjectSupport#initApplicationContext

@Override
protected void initApplicationContext(ApplicationContext context) {
   super.initApplicationContext(context);
   if (this.servletContext == null && context instanceof WebApplicationContext) {
      this.servletContext = ((WebApplicationContext) context).getServletContext();
      if (this.servletContext != null) {
         initServletContext(this.servletContext);
      }
   }
}

那么是在什么時(shí)候觸發(fā)的這個方法呢。

由于過程是在太復(fù)雜遏餐,只把最后幾步貼出來伦腐。

1.org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
2.org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitialization
3.org.springframework.context.support.ApplicationContextAwareProcessor#postProcessBeforeInitialization
4.org.springframework.context.support.ApplicationContextAwareProcessor#invokeAwareInterfaces

最后在invokeAwareInterfaces這個方法里調(diào)用setApplicationContext
如果感興趣的可以自己打好斷點(diǎn)去Debugger跟一下,對于整體spring的裝載機(jī)制都會有一個比較基礎(chǔ)的理解。

if (bean instanceof ApplicationContextAware) {
   ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}

其實(shí)這么復(fù)雜的過程失都,用一句簡單的話總結(jié)就是Spring-boot的一個關(guān)于在訪問 ServletContext的一個回調(diào)接口柏蘑,來自定義初始化。

總結(jié)

以上的內(nèi)容其實(shí)有點(diǎn)難懂粹庞,感興趣的小伙伴可以仔細(xì)研究一下咳焚,畢竟理解Spring比使用API接口要難得多。共勉加油庞溜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末革半,一起剝皮案震驚了整個濱河市碑定,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌又官,老刑警劉巖延刘,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異六敬,居然都是意外死亡碘赖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門普泡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來审编,“玉大人撼班,你說我怎么就攤上這事割笙∪ㄉ眨” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵般码,是天一觀的道長乱顾。 經(jīng)常有香客問我板祝,道長,這世上最難降的妖魔是什么走净? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任券时,我火速辦了婚禮伏伯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘说搅。我一直安慰自己,他們只是感情好适肠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布候引。 她就那樣靜靜地躺著,像睡著了一般澄干。 火紅的嫁衣襯著肌膚如雪柠傍。 梳的紋絲不亂的頭發(fā)上息尺,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機(jī)與錄音徐紧,去河邊找鬼炭懊。 笑死,一個胖子當(dāng)著我的面吹牛侮腹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愈涩,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼加矛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了斟览?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤已烤,失蹤者是張志新(化名)和其女友劉穎妓羊,沒想到半個月后胯究,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體躁绸,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涨颜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年茧球,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弹灭。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖穷吮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情八回,我是刑警寧澤驾诈,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站管引,受9級特大地震影響闯两,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漾狼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一邦投、第九天 我趴在偏房一處隱蔽的房頂上張望伤锚。 院中可真熱鬧志衣,春花似錦、人聲如沸狞洋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽借嗽。三九已至转培,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浸须,已是汗流浹背邦泄。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工裂垦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人特碳。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓企量,卻偏偏與公主長得像,于是被迫代替她去往敵國和親届巩。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)瘾英,斷路器,智...
    卡卡羅2017閱讀 134,671評論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,823評論 6 342
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架但惶,建立于...
    Hsinwong閱讀 22,414評論 1 92
  • 入門 介紹 Spring Boot Spring Boot 使您可以輕松地創(chuàng)建獨(dú)立的膀曾、生產(chǎn)級的基于 Spring ...
    Hsinwong閱讀 16,890評論 2 89
  • SpringMVC原理分析 Spring Boot學(xué)習(xí) 5阳啥、Hello World探究 1、POM文件 1察迟、父項(xiàng)目...
    jack_jerry閱讀 1,296評論 0 1