一、数据响应
spring mvc 中存在2中响应,一个是数据响应(常用于API开发),另一种是页面响应(常用于单体项目的开发)。
JSON的响应
spring boot 中只要引入了 web 启动器,就会自动的引入json的相关功能。在接口上标注@ResponseBody即可。
返回值解析器,一共15中:
处理返回值:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//寻找返回值处理器
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
跟参数处理器一样,返回值处理器也是先判断是否支持,然后在调用返回值处理器进行处理。
支持的返回值:
返回值处理,利用MessageConverters进行处理:
@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);
}
内容协商:
- 请求方回根据自己的实际情况,告诉服务器端,需要什么类型的数据。
- 服务器根据自身能力,生成请求方需要的数据
- 根据确定的数据类型,选择对应类型的消息转换器,消息转换器的作用就是是否支持将class类型的对象转换成MediaType类型的数据。
- 默认的消息转换器,有以下几个:
最终使用MappingJackJon2HttpMessageConverter,转换成了json中。注意一点的是MappingJackJon2HttpMessageConverter是可以支持所有的返回值类型。
二、内容协商
内容协商就是客户端和服务端根据实际情况,约定双方需要的数据格式。也可能在某种情况下,相同的接口web端需要返回json而手机端需要返回xml,这个时候就可以用到spring mvc的内容协商。
根据客户端接收能力的不同,而返回不同媒体类型的数据。
也可以开启参数方式,参数中包含format=json\xml 就可以返回不同的模式。当然也可以自定义MediaType,也就是重写 MessageConverter
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
接口端会根据 Accept 参数,进行判断返回格式
- application/json 返回json
- application/xml 返回xml
如果同时定义了,就会根据权重进行判断,
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
上面的q=0.9 就是权重,因为xml的权重0.9大于json的权重0.8,所以会有限返回xml。
内容协商的原理:
- 判断当前响应头中是否已经有确定的媒体类型,如果存在直接使用
- 如果不存在,获取客户端支持的内容类型,也就是获取Accept字段
- contentNegotiationManager 内容协商管理器,默认使用基于请求头的策略
- 之前将的format参数,就是基于参数策略
- 遍历循环当前系统中全部的MessageConverter(默认10中+xml),看谁支持操作返回值
- 找到支持操作返回值的Converter,把这些Converter的媒体对象收集起来
- 匹配客户端需要的和服务器能够处理的媒体类型,进行最佳匹配
- 用支持将对象转为最佳媒体的Converter进行转换
自定义MessageConverter
再次整理一下内容协商的流程:
- 在方法或者类型标注@ResponseBody证明响应的是数据,spring mvc 会调用 RequestResponseBodyMethodProcessor处理
- RequestResponseBodyMethodProcessor通过MessageConverter处理
- 所有的MessageConverter合起来支持大部分的媒体类型操作(读、写)
- 内容协商找到最终的MessageConverter处理返回数据
自定义MessageConverter过程:
- 添加自定义的MessageConverter
- spring mvc 遍历所有MessageConverter
- 找到支持自定义类型返回的Converter
WebMvcConfigurationSupport,xml的Converter支持源码,发现系统中存在相关类,直接导入Converter:
在spring boot 配置spring mvc 相关内容,只有一个入口就是 WebMvcConfigurer,当使用者期望配置相关Converter的时候,直接向容器中注入@Bean:
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//添加自定义的Converter
converters.add(new MgMessageConverter());
}
};
}
自定义Converter:
package com.example.demo.converter;
import com.example.demo.dto.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.util.List;
/**
* 自定义的Converter
*/
public class MgMessageConverter implements HttpMessageConverter<Person> {
/**
* 是否能够读取,按照自动以类型读
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
/**
* 是否能读
* 是 person类型的就能读
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 支持媒体类型,服务要知道这个Converter能够支持哪些媒体类型
* application/x-mg
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-mg");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
/**
* 数据写出
* @param person
* @param contentType
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getName() + ";" + person.getSex() + ";" + person.getAge();
//数据写到流中
outputMessage.getBody().write(data.getBytes());
}
}
客户端请求:
跟踪源码可以看到,获取客户端的媒体类型为:
服务器可以处理消息的Converter:
以上是使用请求头的方式,下面可以说一下使用参数方式。
因为参数协商策略只支持json和xml,所以就需要自定义策略:
自定义策略也十分的简单:
package com.example.demo.conf;
import com.example.demo.converter.MgMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
@Configuration(proxyBeanMethods = false)
public class WebMvcConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
/**
* 自定义Converter
* @param converters
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//添加自定义的Converter
converters.add(new MgMessageConverter());
}
/**
* 自定义协商管理
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//支持的媒体类型
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
mediaTypes.put("xml", MediaType.APPLICATION_ATOM_XML);
mediaTypes.put("mg", MediaType.parseMediaType("application/x-mg"));
ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy));
}
};
}
}
跟踪代码可以看到,已经从原来的2中支持到现在的3中了:
在自定义功能的时候,小心默认功能失效,这个时候需要dubeg源码,查看哪里出现问题,解决即可。