本文是學(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
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);
}
}
流程圖
源碼解讀
我們可以根據(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接口要難得多。共勉加油庞溜。