17.数据响应和内容协商
什么是内容协商,可以根据客户端可接收的类型,给客户端返回不同格式的报文,例如客户端可以接受json的报文,就返回json的报文,当客户端可以接受xml的报文,就返回xml的报文!
1.响应json数据
响应json数据:jackson.jar+@ResponseBody
在web启动器中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
帮我们自动引入json的包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.3</version>
<scope>compile</scope>
</dependency>
如何往前台返回json字符串,重点是@ResponseBody标签
示例:
1.在方法上加上@ResponseBody注解
@Controller
public class ResponseTestController {
@GetMapping("/test/person")
@ResponseBody
public Person getPerson(){
Person person=new Person();
person.setName("吴孟达");
person.setAge(18);
return person;
}
}
2.在控制类上加@RestController注解;该注解里面就是@Controller和@ResponseBody
@RestController
public class ResponseTestController {
@GetMapping("/test/person")
public Person getPerson(){
Person person=new Person();
person.setName("吴孟达");
person.setAge(18);
return person;
}
}
底层原理:
底层解析返回值类型式,会有很多种返回值解析器,看哪种解析器可以解析该返回值类型
2.响应xml的数据
1.先导入xml的配置文件
<!--导入xml的依赖-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
控制类代码:
测试样例:
@RestController--->注意是 @RestController标签,包含了@Controller和@ResponseBody标签,@ResponseBody这个标签是必须!!!
public class EntityController {
@RequestMapping("/savePerson")
public Person savePerson(){
Person person = new Person(18,"吴孟达",new Pet(16, "刘丹"));
return person;
}
}
结论发现:如果导入了xml的包,响应的是xml的报文
底层原理分析:起点是DispatcherServlet.doDispatch()方法!
在底层处理方法返回值时:
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);方法细节
内容协商关键代码:
protected <T> void writeWithMessageConverters(){
...
//重要点1:查询出客户端可以支持的类型:即获取的是请求头中accept中的值!
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
具体细节如下:一层一层嵌套,最终调用的是该方法,
public List<MediaType> resolveMediaTypes(NativeWebRequest request){
...
//重要点2:发现其底层调用的就是获取请求头中的accept字段:详情参考截图
String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
...
}
//重要点3:获取可以产生的类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
具体细节如下:
protected List<MediaType> getProducibleMediaTypes(){
//重要点4:遍历所有的消息转换器:messageConverters里有11项:
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
}
...
//重要点5:嵌套循环,匹配请求的数据类型和可以产生的数据类型
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
//重要点6,按照权重去排序mediaTypesToUse(上面嵌套循环):
//什么是权重:例如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,q=0.8就是权重
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
//重要点7.根据权重,找到最优的返回类型
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
//重要点8:第二次遍历messageConverters(消息转换器)
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
...
body:写的内容:Person(age=18, name=吴孟达, pet=Pet(age=16, name=刘丹))
targetType:class cn.com.springboot.entity.Person
selectedMediaType:application/xhtml+xml
//按照匹配结果写出
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
...
}
}
获取客户端支持的类型:
2.获取可以生产的数据类型
开启浏览器参数方式的内容协商功能
结论:发现springboot底层可以根据客户端接收类型的不同而返回不同格式的数据,
使用postman可以直接设置请求头中的accpt的值,那浏览器如何操作呢,
或者说,如何控制让springboot既可以返回json也可以返回xml呢
第一步:
在springboot的配置文件中添加参数内容协商模式:默认false
spring.mvc.contentnegotiation.favor-parameter=true
测试:在请求路径后加参数:format=返回格式
1. 当请求路径是http://localhost:8080/savePerson?format=json
返回的是:{"age":18,"name":"吴孟达","pet":{"age":16,"name":"刘丹"}}
2.当请求路径是http://localhost:8080/savePerson?format=xml
返回是:
<Person>
<age>18</age>
<name>吴孟达</name>
<pet>
<age>16</age>
<name>刘丹</name>
</pet>
</Person>
参数内容协商的原理:
其他细节和上述一样
...
//重要点1:获取支持的内容类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
细节如下:
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request){
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
重要点2:
如果没有开启参数内容协商模式时:
contentNegotiationManager里只有一个值:HeaderConteNegotiationStrategy(请求头内容协商策略)
当开启了参数内容协商功能时:
contentNegotiationManager里只有一个值:0.ParameterCnterntNegotiationStrategy(请求参数内容协商策略) 1.HeaderConteNegotiationStrategy(请求头内容协商策略)
上述方法细节:
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
//重要点3:循环这两种策略,第一个是参数策略,拿到就返回,不执行第二个
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
//重要点4:请求参数内容协商如何获取内容类型呢?
即上述方法详情:最终返回的是获取请求参数中的format参数,并处理作为内容类型返回
request.getParameter("format");
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
...
自定义返回内容类型
场景:如果我们需要返回的内容是以类属性返回加分号隔开返回
1.创建我们自己的MessageConverter(消息转换器)
重点1:实现HttpMessageConverter接口,重写里面的方法
public class WmdMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
重点2:判断什么情况下使用自定义的信息解析器:此处判断是perosn的父类或者本类时使用该解析器
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
//是否是perosn类的父类
Boolean isAssignable=clazz.isAssignableFrom(Person.class);
return isAssignable;
}
//服务器要统计所有MessageConverter都能写出哪些内容类型
//
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-wmd");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
//重点4:具体写出的内容格式
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议的写出
String data="person:"+person.getName()+";"+person.getAge()+";pet:"+person.getPet().getName()+";"+person.getPet().getAge();
//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
2.将自定义消息解析器加到容器中:
//重点1:@Configuration注解标明当前是sprinboot的配置类
@Configuration
public class WebConfig {
//重点2:springboot web mvc所有的定制化功能都在改类下生成,即重写WebMvcConfigurer类中的方法即可!
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new WmdMessageConverter());
}
...
重写方法,完成其他的定制化功能
};
}
}
具体的原理如下:
重点1:获取客户端支持的内容类型,即获取请求中的accept中的内容
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
具体细节如下:
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
//因为开启了参数内容协商功能,所以此处的strategies(内容解析策略有两个:0.ParameterCnterntNegotiationStrategy(请求参数内容协商策略) 1.HeaderConteNegotiationStrategy(请求头内容协商策略))
//参数解析策略会获取请求参数中的format字段,为空,会判断进入到请求头策略中,获取到请求头中accept的值,处理后为:application/x-wmd
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
重点2:获取springboot可以产生的内容类型:
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
具体的方法细节如下:
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
....
//重点3:遍历所有的消息解析器:此时的messageConverters有12项,包含了自定义的WmdMessageConverter,具体如图
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
//此处会调用自定义WmdMessageConverter的canWrite方法,满足条件后,调用WmdMessageConverter的getSupportedMediaTypes方法
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
}
重点3:客户端支持的内容类型和springboot可以产生的内容类型进行嵌套循环匹配,得到最终支持内容类型为:application/x-wmd
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
重点4:最终调用,
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//遍历12种消息转换器,进行canWrite的判断,最后会定位到自定义WmdMessageConverter的canWrite方法,返回true
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
...
//重点5:调用自定义的write方法,将内容写出!
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
....
}
此时,有个问题:如何在参数上设置自定义的内容协商呢
问题:
当请求是http://localhost:8080/savePerson?format=wmd时
后台的代码:
重点1:获取客户端支持的内容类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
详情:
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
重点2:此时的strategies(策略)有两个:0.ParameterCnterntNegotiationStrategy(请求参数内容协商策略) 1.HeaderConteNegotiationStrategy(请求头内容协商策略))
//请求参数处理逻辑是:获取到请求参数中的format值,并从参数策略中的mediatypes根据wmd作为key,去获取对应的内容格式,但mediatypes中的值只有两个,详情如图:
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
所以解决思路是,往参数解析器中的mediaTypes中添加自己的wmd-->application/x-wmd
如何做呢,牵扯到springmvc的定制:
1.在自定义的springboot配置类中添加重点1.2
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
重点1:添加自定义的内容解析器:处理application/x-wmd的内容格式
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new WmdMessageConverter());
}
重点2:添加自定义的参数内容协商策略
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypeMap=new HashMap<>();
//指定哪些参数对应哪些媒体类型
mediaTypeMap.put("json", MediaType.APPLICATION_JSON);
mediaTypeMap.put("xml", MediaType.APPLICATION_XML);
mediaTypeMap.put("wmd", MediaType.parseMediaType("application/x-wmd"));
ParameterContentNegotiationStrategy parameterStrategy=new ParameterContentNegotiationStrategy(mediaTypeMap);
configurer.strategies(Arrays.asList(parameterStrategy));
}
};
}
}
那如解决同时请求头内容协商和请求参数内容协商呢?
只需
@Configuration
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new WmdMessageConverter());
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> mediaTypeMap=new HashMap<>();
//指定哪些参数对应哪些媒体类型
mediaTypeMap.put("json", MediaType.APPLICATION_JSON);
mediaTypeMap.put("xml", MediaType.APPLICATION_XML);
mediaTypeMap.put("wmd", MediaType.parseMediaType("application/x-wmd"));
ParameterContentNegotiationStrategy parameterStrategy=new ParameterContentNegotiationStrategy(mediaTypeMap);
//重点1:添加请求头内容协商即可
HeaderContentNegotiationStrategy headerStrategy=new HeaderContentNegotiationStrategy();
configurer.strategies(Arrays.asList(parameterStrategy,headerStrategy));
}
};
}
}
但是这时,只有一个参数请求策略,没有请求头处理策略,即如果没有设置参数处理策略或者传入错误参数(format=aaa),返回的都是json
这是什么原因呢?