本文是学习了小马哥在慕课网的课程的《Spring Boot 2.0深度实践之核心技术篇》的内容结合自己的需要和理解做的笔记。
上一篇 Spring--视图内容协商(一) 讲解了如何配置内容协商以及spring-boot是如何配置关联匹配策略的。现在让我们来自己走一遍协商流程,加深印象。
由于现在主要都是Restful API形式的请求,就主要把HeaderContentNegotiationStrategy
这个默认加载的视图协商处理简单的记录和解释一下。
这里我们就做一个简单的demo,根据请求头的格式不同来返回不同的渲染引擎模版,如果是Accept:text/xml 则返回JSP,如果是Accept:text/html 则返回 thymeleaf。
代码
针对上一篇的代码有所修改,索性全部贴上。
项目目录
pom.xml
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
javax.servlet
javax.servlet-api
javax.servlet
jstl
org.springframework.boot
spring-boot-starter-tomcat
org.apache.tomcat.embed
tomcat-embed-jasper
org.springframework.boot
spring-boot-maven-plugin
WebMvcConfig
/**
* 视图协商相关配置
*/
@Configuration //配置
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 配置新的JSP视图解析
*/
@Bean
public ViewResolver myViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
viewResolver.setOrder(Ordered.LOWEST_PRECEDENCE-10);
viewResolver.setContentType("html/xml;charset=UTF-8");
return viewResolver;
}
/* @Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorParameter(true).favorPathExtension(true);
}*/
/**
* 解决在IDEA下maven多模块使用spring-boot跳转JSP 404问题
* @return
*/
@Bean
public WebServerFactoryCustomizer customizer() {
return (factory -> {
factory.addContextCustomizers(context -> {
//当前webapp路径
String relativePath = "springboot-restful/src/main/webapp";
File docBaseFile = new File(relativePath);
if(docBaseFile.exists()) {
context.setDocBase(new File(relativePath).getAbsolutePath());
}
});
});
}
}
HelloWorldController
/**
* 简单controller
*/
@Controller
public class HelloWorldController {
@RequestMapping("/")
public String index() {
System.out.println("执行HelloWorldController中的index()方法");
return "index";
}
}
引导类SpringBootRestfulBootStrap
/**
* Spring-boot 启动引导类
*/
@SpringBootApplication
public class SpringBootRestfulBootStrap {
public static void main(String[] args) {
SpringApplication.run(SpringBootRestfulBootStrap.class,args);
}
}
application.properties
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
spring.thymeleaf.prefix = classpath:/templates/thymeleaf/
spring.thymeleaf.suffix = .html
spring.thymeleaf.cache = false
thymeleaf---index.html
thymeleaf Page
hello world
JSP---index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
JSP Page
Hello World
视图协商效果
在探讨spring是如何进行请求头协商策略(HeaderContentNegotiationStrategy
)的之前,我们先来看一下协商结果。
使用postMan测试效果。
1.请求头格式是是 text/html 。让我们看一下返回的是不是thymeleaf的index.html。
2.请求头格式是是 text/html 。让我们看一下返回的是不是jsp的index.jsp。
我们可以看到,结果符合预期,那么接下来让我们深入的理解一下 Spring的视图协商流程。
理解视图协商流程
在具体理解HeaderContentNegotiationStrategy
流程之前,我们要先要明白两个关键点。
- HTTP Accept 请求头 与 ViewResolver的 Content-Type 匹配
- 匹配规则顺序
-
ViewResolver
匹配 -
MediaType
匹配
-
HTTP Accept 请求头 与 ViewResolver的 Content-Type 匹配
对于HTTP Accept 请求头相信大家都知道在此就不多做介绍了,对于ViewResolver
的Content-Type
在这里我们就拿我们自定义的视图解析器来看一下就明白了。
com.web.configuration.WebMvcConfig#myViewResolver
@Bean
public ViewResolver myViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setViewClass(JstlView.class);
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
//视图解析器顺序
viewResolver.setOrder(Ordered.LOWEST_PRECEDENCE-10);
//配置视图解析器的ContentType
viewResolver.setContentType("html/xml;charset=UTF-8");
return viewResolver;
}
我们可以看到 在自定义的视图解析器中,我们定义了JstlView 视图渲染引擎,也就是JSP 的渲染引擎。
重点是我们将 自定义的视图解析器的Content-Type 的内容设置为 html/xml;charset=UTF-8
。这样就可以通过浏览器请求的Accept 类型进行匹配。具体是如何进行匹配的稍后我们一起来看源码。
而对于匹配规则,我们稍后看一下源码就可以理解了。
视图协商源码简单解读
在之前已经介绍了SpringMvc的架构流程,我们在这里直接看重点,具体想知道如何跳进这段源码的,可以看看之前的博文。
我们先用浏览器进行简单的请求 localhost:8080.
首先我们来看一下org.springframework.web.servlet.DispatcherServlet#resolveViewName
我们可以看到 在这里视图解析器有6个 而排在第一个的就是我们的视图协商解析器, 在上一篇文章也介绍过,这个解析器中 包含了非他意外的所有解析器。我们接着往下看,进入到resolveViewName
方法中。
在这个方法中,首先是通过 getMediaTypes
获取请求头的所有媒体类型。我们来细看一下这个getMediaTypes
方法。步骤解释已在注释里说明。
@Nullable
protected List getMediaTypes(HttpServletRequest request) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
try {
ServletWebRequest webRequest = new ServletWebRequest(request);
//1.获取媒体类型
List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
/2./获取程序可生成的媒体类型 在这里我们默认为所有
List producibleMediaTypes = getProducibleMediaTypes(request);
Set compatibleMediaTypes = new LinkedHashSet<>();
//3.针对可生成的媒体类型进行匹配
for (MediaType acceptable : acceptableMediaTypes) {
for (MediaType producible : producibleMediaTypes) {
if (acceptable.isCompatibleWith(producible)) {
compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible));
}
}
}
//4.最终生成可接受的请求头
List selectedMediaTypes = new ArrayList<>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(selectedMediaTypes);
if (logger.isDebugEnabled()) {
logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " +
"and producible media types " + producibleMediaTypes + ")");
}
return selectedMediaTypes;
}
catch (HttpMediaTypeNotAcceptableException ex) {
return null;
}
}
在这里我们需要注意的是,注释1 的获取媒体类型的方法。这个方法是通过Spring 配置的策略来遍历获取最合适的媒体类型集合。
我们顺便来看一下 org.springframework.web.accept.HeaderContentNegotiationStrategy#resolveMediaTypes
的方法就明白这个策略就是来匹配请求头的策略。
@Override
public List resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {
//获取请求头的所有媒体类型字符串数组
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}
List headerValues = Arrays.asList(headerValueArray);
try {
//解析成Spring 自己定义的媒体类型结合
List mediaTypes = MediaType.parseMediaTypes(headerValues);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
媒体类型获取完成后接下来我们就要获取最佳的视图匹配。我们接着往下看 我们通过ViewName
来获取 候选的视图。
选出候选的视图解析器之后,那么重中之重的地方就来了,那就是选择最佳的视图解析器。我们来看一下 org.springframework.web.servlet.view.ContentNegotiatingViewResolver#getBestView
的代码
重要的代码部分我已经拿红框圈出,在这里我们可以一步一步的了解到 Spring 是如何选出最佳的视图解析器的。
我们可以看到通过两层校验
- 第一步是判断视图解析器是设置了
ContentType
- 第二步是通过
isCompatibleWith()
方法来进行校验匹配。
第一个视图解析器就是我们自定义的 JSP视图解析器,而 我们请求的是 mediaType:"text/html"
,而我们自定义的JSP视图解析器中的 Content-Type 是 html/xml;charset=UTF-8
因此不匹配。
那么我们来看一下第二个 视图解析器 也就是 Thymeleaf解析器 。
我们可以看到 请求的 mediaType:"text/html"
和 Thymeleaf解析器中的 Content-Type html/html;charset=UTF-8
可以匹配上,那么 Spring就会选定它为最佳的视图解析器。
总结
Spring 基本的内容协商操作流程已经介绍完了。可以参照上一篇的 流程图来看 ,相信会理解的更快。 内容协商的错综复杂关系 需要一定时间的研究才能初步理解,相信只要坚持,理解Spring的脚步就越来越近了。
DEMO地址