sss当通过浏览器或者PostMan模拟请求时, 我们首先是执行方法(获取解析参数,执行方法体),然后开始处理返回值,也就是我们今天的主题,响应处理(数据响应与内容协商)。其逻辑都在一个方法中:
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest); , 这是我们今天分析的主题。
数据响应与内容协商
sssdsadasdasdasddasadaasd
响应JSON
JSON1 jackson.jar+@ResponseBody
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
#web场景自动引入了json场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId> #这里面有相关的jackson依赖
<version>2.3.7.RELEASE</version>
<scope>compile</scope>
</dependency>
sss● 正因为有这样的配置,给前端自动返回json数据;
测试代码:
sss● 测试代码:
@Controller
public class ResponseTestController {
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setBirth(new Date());
person.setUserName("Zhangsan");
return person;
}
}
sss● 结果:
返回值解析器:
sss● 当我们在执行前目标方法时(获得参数,解析参数,执行方法体,返回值,响应返回值),我们需要先获取参数解析器和返回值解析器,关于参数获得解析的相关方法、逻辑在前几篇文章已经介绍,我们重点看返回值处理器(一共15个):
sssddsadsaasdasd
ssdasds【SpringMVC到底支持哪些返回值?】:(网图)
ssddasdasdasd
ssdsdasd有 @ModelAttribute 且为对象类型的 @ResponseBody 注解 —> RequestResponseBodyMethodProcessor;
sss● 获得参数处理器和返回值处理器后,执行下图方法(我们在文章开始处就声明了该方法)(注意其中参数,首先是执行完方法体后的Object返回对象,然后是该返回对象的类型,最后是mav容器和requst请求):
sssdasdas
sss● 点击进入handleReturnValue()方法,有两个核心方法:①、遍历返回值处理器,获取相应的返回值处理器 (我们获得的返回值处理器是 RequestResponseBodyMethodProcessor) ②、利用匹配的返回值i处理器对返回值进行响应。
sss● 我们重点分析第二个方法,看其源码:
sss【注】:inputMessage、outputMessage就是将原生的webReques包装起来,对应着请求、响应。
sss第二个方法writeWithMessageConverters(): 利用MessageConverter(消息转换器)将数据写为 json(当然也可以是别的形式),该方法重点是内容协商,就是如何选用消息转换器MessageConverter,然后转成什么样的格式返回?我们先简单分析这个方法,然后在下一节深入研究:
sss【上图问题解答 1】:① 注释: 是person类型,不是字符串类型,valueTypre是返回值类型,target是目标类型,所以赋值位置是在else后。
sss【上图问题解答 2】:②注释: 返回值类型是不是资源类型,比如流数据。
sss【上图问题解答 3】:③~④注释中间:会先通过OutputMessage的响应头看有没有内容类型,如果这个请求已经被处理过,则可能响应头里就有内容类型,这时候则直接使用,也就是selectedMedaiTypre = contentType,而不会执行④⑤⑥⑦注释所在的代码。
sss【上图问题解答 4】:⑧注释:到底应不应该优化呢?如果我们在注释④的位置之前我们得值响应头有内容类型,所以就不会执行⑤这个方法,就不会有缓存,所以还需要重新遍历所有的消息转换器。
sss【上图问题解答 5】:对于⑨前面的this.addContentDispositionHeader(inputMessage, outputMessage),是往outputMessage中加上ServletServerHttpResponse,不过目前为空。在⑨中添加headers,key为"Content-Type",value为 selectedMediaType.
sss【注 1】:默认的MessageConverter:
sasdadsadaasdsadasdasdsdassass
dsdssss0 - 只支持Byte类型的dsdssss1 - Stringdsdssss2 - Stringdsdssss3 - Resource
dsdssss4 - ResourceRegion
dsdssss5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
dsdssss6 - MultiValueMap
dsdssss7.8 - true sss- true:无论什么类都返回true,换句话说,就是可以将任何类型的对象转换为浏览器所想要的数据类型;dsdssss9 - 支持注解方式xml处理的。
sss【注 2】:MessageConverter规范:
dsdssssHttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
dsdsssseg:Person对象转为JSON。或者 JSON转为Person
sss【注 3】:最终消息转换器 MappingJackson2HttpMessageConverter 把对象转为JSON(利用底层的jackson的objectMapper转换的)
sss【注 4】:
sss总结:返回值解析器原理:(两个方法,先看是否支持,再处理!!!)
ssdsdsdsdsdsdsdsdss
sss● 1、返回值处理器判断是否支持这种类型返回值 supportsReturnType
sss● 2、返回值处理器调用 handleReturnValue 进行处理
sss● 3、RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。
ssdsdss ○ 1. 利用 MessageConverters 进行处理 将数据写为json
sdsdsdsdsdss ■ 1、内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
ssdsdsddsdss ■ 2、服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
ssddsdsdsdss ■ 3、SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ,看谁能处理?
dsddssss
ssdsdsdsdsdsdsdss ● 1、得到MappingJackson2HttpMessageConverter可以将对象写为json
ssddssdsdsddssdss ● 2、利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
内容协商:
sss在数据响应的过程中,我们要考虑客户端接收什么类型的数据,而服务器又可以生成什么样的数据,是根据什么评判的?接下来我们开始分析。
测试: 根据客户端接收能力不同,返回不同媒体类型的数据。
sss引入xml依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
sss浏览器测试返回xml:
sss为什么浏览器会直接返回xml格式的数据 ?(因为xml优先级高)
sss【注】: postman分别测试返回JSON和XML。只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
sss【注1】: 若客户端无法解析服务端返回的内容,即媒体类型未匹配,那么响应406
sss【注2】:为了方便内容协商,开启基于请求参数的内容协商功能:
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
sdsdss发送请求 ① : http://localhost:8080/test/person?format=json
sdsdss发送请求 ② : http://localhost:8080/test/person?format=xml
sss【注3】: 优先级为:后缀 > 请求参数 > HTTP首部Accept。具体看SpringMVC配置相关讲解
内容协商原理:
sss当我们在获取获取客户端可接受的数据类型时,相应代码表现为:
List<MediaType> acceptableTypes = this.getAcceptableMediaTypes(request);
====》点击进入查看源码:
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)) }
=====》我们发现我们是通过"内容协商管理器"来进行解析媒体类型的,那是如何解析参数的呢?
ssds我们发现,“内容协商管理器”中存放着不同的策略,我们根据不同的策略来解析客户端可接受的媒体类型,通过源码可知,我们会遍历所有的策略,比如有两个策略,我们通过第一个策略先获得媒体类型,如果该类型包含了所有媒体类型(也就是范围没有缩小),则继续用下一个策略寻找媒体类型。源码如下:
asdasdasdsaddsasss
ssasdasdasdsaadsasd
sss【注4】:服务器默认的策略其实只有一个,就是根据“请求头”来进行解析的,而如果想根据参数来解析,那么需要进行相关配置,具体请看【注2】
sss【注5】:我们通过参数内容协商策略发现,默认的媒体类型只有"xml"和"json",也就是说我们如果想通过这种策略接收别的媒体方式是不行的。(后面自定义会说)
sss接下来我们通过不同的设置来看不同的策略具体是怎么解析参数的(就是上图中的 ②)?
①、基于请求头的策略是如何解析客户端可以获得数据类型的?
②、基于参数类型的策略是如何解析客户端可以获得什么数据类型的?sssⅰthis.getMediaTypeKey(webRequest) ? 如何根据请求获得参数名?
ssssdas
sdsss【注6】:通过format,我们找对应的参数是多少:
sssⅱ 如何根据参数名获得相应的参数媒体类型呢?
自定义 MessageConverter
sss实现多协议数据兼容。json、xml、x-test(自定义)SpringMVC自定义功能只需要给容器中添加一个WebMvcConfiguration
内容协商 - 基于请求头 Accept
@Controller
public class ResponseTestController {
/**
* 1、浏览器发送请求直接返回xml 【application/xml】 jackonXmlConverter
* 2、如果是ajax请求,返回json 【application/json】 jackonJsonConverter
* 3、如果硅谷app发请求,返回自定义协议数据 [application/x-test] xxxConverter
* 属性值1:属性值2;
* 步骤:
* 1、添加自定义的MessageConverter进系统底层
* 2、系统底层就会统计出所有MessageConvcerter能操作哪些类型
* 3、客户端内容协商[guigu--->guigu]
* @return
*/
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setBirth(new Date());
person.setUserName("Zhangsan");
return person;
}
}
sss①:自定义converter:
public class GuiguMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> aClass, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> aClass, MediaType mediaType) {
return aClass.isAssignableFrom(aClass);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}
@Override
public Person read(Class<? extends Person> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getUserName() + ";" + person.getAge() + ";" + person.getBirth();
OutputStream body = httpOutputMessage.getBody();
body.write(data.getBytes());
}
}
sss②:将自定义的converter添加到WebConfig
@Configuration(proxyBeanMethods = false)
public class config /*implements WebMvcConfigurer*/ {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}
sss利用postman发送请求,结果如下:
内容协商 - 基于请求参数 format
sss基于请求参数的内容协商默认只支持两种类型 - xml、json,如果还想再增加一种类型,那么我们应该写这个内容协商管理器:
@Configuration(proxyBeanMethods = false)
public class config /*implements WebMvcConfigurer*/ {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes
Map<String, MediaType> mediaTypes = new HashMap<>();
//指定支持哪些参数对应的哪些媒体类型
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
HeaderContentNegotiationStrategy headStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterStrategy,headStrategy));
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
};
}
}
sss【注】:当我们自定义了基于请求参数的内容协商策略后,就无法解析基于请求头的内容协商了,如果这时发起基于请求头的内容协商,不管请求头要求返回什么类型的数据,都会默认返回JSON。这时,我们可以自定义基于请求头的策略,或者将原本的基于请求头的策略添加到内容协商管理器中。