数据响应与内容协商
文章目录
一、响应JSON
解析request要找到参数解析器和返回值处理器,而对于@ResponseBody注解的方法,其实就是其对应的返回值处理器再起作用
1、返回值解析器
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>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
给前端自动返回json数据;
返回值解析器
其中处理器RequestResponseBodyMethodHandler
,就是我们使用@ResponseBody时,使用的处理器,底层如下:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
那么拿到对应的返回值处理器后,Spring MVC是如何对返回值进行操作的呢?
debug发现,处理@ResponseBody,会调用以下方法:
RequestResponseBodyMethodProcessor
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
public boolean supportsReturnType(MethodParameter returnType) {
return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class);
}
2、返回值解析器原理
a. 返回值处理器判断是否支持这种类型返回值
b. supportsReturnType
c. 返回值处理器调用 handleReturnValue 进行处理
RequestResponseBodyMethodProcessor
可以处理返回值标了@ResponseBody 注解的。
- 利用 MessageConverters 进行处理 将数据写为json
内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型) - 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
- SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter
,看谁能处理?
✌️ 得到MappingJackson2HttpMessageConverter可以将对象写为json
✌️ 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
3、Spring MVC支持的返回值类型
writeWithMessageConverters
部分源码:
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class valueType;
Object targetType;
//先判断要返回的值是不是字符串类型
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
} else {
//拿到要返回的对象
body = value;
//拿到要返回对对象的类型
valueType = this.getReturnValueType(value, returnType);
targetType = GenericTypeResolver.resolveType(this.getGenericType(returnType), returnType.getContainingClass());
}
//再判断返回值类型是不是资源类型(流数据),如果是,就调用下面对流数据处理的逻辑,例如直接响应
if (this.isResourceType(value, returnType)) {
outputMessage.getHeaders().set("Accept-Ranges", "bytes");
if (value != null && inputMessage.getHeaders().getFirst("Range") != null && outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource)value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
} catch (IllegalArgumentException var19) {
outputMessage.getHeaders().set("Content-Range", "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
//(媒体类型)内容协商,浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
//服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
} else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes = this.getAcceptableMediaTypes(request);
List<MediaType> producibleTypes = this.getProducibleMediaTypes(request, valueType, (Type)targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList();
Iterator var15 = acceptableTypes.iterator();
MediaType mediaType;
while(var15.hasNext()) {
mediaType = (MediaType)var15.next();
Iterator var17 = producibleTypes.iterator();
while(var17.hasNext()) {
MediaType producibleType = (MediaType)var17.next();
if (mediaType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(this.getMostSpecificMediaType(mediaType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
var15 = mediaTypesToUse.iterator();
//遍历协商,得到可以返回的数据类型
while(var15.hasNext()) {
mediaType = (MediaType)var15.next();
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using '" + selectedMediaType + "', given " + acceptableTypes + " and supported " + producibleTypes);
}
}
HttpMessageConverter converter;
GenericHttpMessageConverter genericConverter;
label159: {
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
Iterator var22 = this.messageConverters.iterator();
//遍历所有的HttpMessageConverter(看是否支持将 此 Class类型的对象,转为MediaType类型的数据,过程是可逆的)
//得到可匹配的消息转换器,支持将对象转成Json数据,这就是关键的核心
//最终得到MappingJackson2HttpMessageConverter可以将对象写为json
while(var22.hasNext()) {
converter = (HttpMessageConverter)var22.next();
genericConverter = converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter)converter : null;
if (genericConverter != null) {
if (((GenericHttpMessageConverter)converter).canWrite((Type)targetType, valueType, selectedMediaType)) {
break label159;
}
} else if (converter.canWrite(valueType, selectedMediaType)) {
break label159;
}
}
}
}
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
4、HttpMessageConverter原理
(1)MessageConverter规范:
HttpMessageConverter: 看是否支持将 此 Class类型的对象,转为MediaType类型的数据。
例子:Person对象转为JSON。或者 JSON转为Person
(2)默认的MessageConverter:
0 - 只支持Byte类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式xml处理的。
最终 MappingJackson2HttpMessageConverter
把对象转为JSON(利用底层的jackson的objectMapper转换的)
附加:文件返回方式
FileSystemResource 是Resource的实现类,所以springMVC最终会调用对应的messageConverter进行处理
@ResponseBody //--RequestResponseBodyMethodProcessor ---> messageConverter
@GetMapping("/he11")
public FileSystemResource file(){
//文件以这样的方式返回看是谁处理的(messageConverter)。
return null;
}
二、内容协商
根据客户端接收能力的不同,返回不同媒体类型的数据
1、引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2、postman分别测试返回json和xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
3、开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
发请求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
打断点,Debug调试:
确定客户端接收什么样的内容类型;
- Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
- 最终进行内容协商返回给客户端json即可。
4、内容协商原理
源代码执行流程分析:
(1)查看MediaType
判断当前响应头中是否已经有确定的媒体类型。MediaType
(2)获取接收的内容类型
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)[application/xml]
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
(3)遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
(4)找到支持操作Person的converter,把converter支持的媒体类型统计出来。
(5)客户端需要[application/xml]。服务端能力[10种、json、xml]
(6)进行内容协商的最佳匹配媒体类型
(7)用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了jackson处理xml的包,xml的converter就会自动进来
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
5、自定义MessageConverter
实现多协议数据兼容。json、xml、x-micah
- @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
- Processor 处理方法返回值。通过 MessageConverter 处理
- 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的 messageConverter;
SpringMVC的什么功能。一个入口给容器中添加一个 WebMvcConfigurer
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
/**
* 不覆盖原来的MessageConverter
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加自定义的MessageConverter
converters.add(new GuiguMessageConverter());
}
}
}
有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。
大家考虑,上述功能除了我们完全自定义外?SpringBoot有没有为我们提供基于配置文件的快速修改媒体类型功能?怎么配置呢?[提示:参照SpringBoot官方文档web开发内容协商章节]
基于请求参数的方式自定义MessageConverter:
自定义请求参数配置策略:
/**
* 自定义内容协商策略
* @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);
//parameterStrategy.setParameterName("ff");
HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterStrategy,headeStrategy));
}
结果: