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
这是什么原因呢?

posted @ 2022-05-11 22:00  努力的达子  阅读(70)  评论(0编辑  收藏  举报