原创

SpringMVC数据响应与内容协商

温馨提示:
本文最后更新于 2022年10月27日,已超过 918 天没有更新。若文章内的图片失效(无法正常加载),请留言反馈或直接联系我

一 响应json

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.5.1</version>
      <scope>compile</scope>
 </dependency>

2. 返回值解析器

image-20220602173056610

springMVC支持图中的15种返回数据

ModelAndView
Model
View
ResponseEntity 
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor

3. 返回值处理流程

1. 进行返回值处理
    this.returnValueHandlers.handleReturnValue(
                    returnValue, getReturnValueType(returnValue), mavContainer, webRequest);

2. 查询返回值处理器
    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);

3. 循环便利容器中的返回值处理器,依次判断该处理支不支持对应的返回值类型
    private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
    boolean isAsyncValue = isAsyncReturnValue(value, returnType);
    for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
            continue;
        }
        if (handler.supportsReturnType(returnType)) {
            return handler;
        }
    }
    return null;
    }
4. 获取到能处理该返回值的处理器
    handler.supportsReturnType(returnType)

5. 使用获取到的处理器处理返回值 
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);

6. 将处理好的数据写出去
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

4. 数据写出流程

writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);

  1. 判断返回值是不是CharSequence(字符序列)类型

  2. 判断返回值是不是isResourceType(资源)类型

  3. 获取原生的HttpServerletRequest,并且从request中获取浏览器支持的返回值类型

    1. HttpServletRequest request = inputMessage.getServletRequest();
    2. acceptableTypes = getAcceptableMediaTypes(request);
    
  4. 获取服务器支持的返回值类型

    List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    
  5. 最终获取到支持的返回值类型

  6. 遍历容器中的消息转换器,使用能支持转换器将消息转换并写出

    for (HttpMessageConverter<?> converter : this.messageConverters) {
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                                                        (GenericHttpMessageConverter<?>) converter : null);
        //判断转换器能处理该消息类型
        if (genericConverter != null ?
            ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
            converter.canWrite(valueType, selectedMediaType)) {
            body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                                               (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                                               inputMessage, outputMessage);
            if (body != null) {
                Object theBody = body;
                LogFormatUtils.traceDebug(logger, traceOn ->
                                          "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                addContentDispositionHeader(inputMessage, outputMessage);
                if (genericConverter != null) {
                    //将消息写出
                    genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                }
                else {
                    ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                }
            }
            else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Nothing to write: null body");
                }
            }
            return;
        }
    }
    

image-20220602175359455

5. 自定义消息转换器HttpMessageConverter

  1. 添加默认的消息转换器的源码 WebMvcConfigurationSupport.class

        /**
         * Adds a set of default HttpMessageConverter instances to the given list.
         * Subclasses can call this method from {@link #configureMessageConverters}.
         * @param messageConverters the list to add the default message converters to
         */
        protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
            messageConverters.add(new ByteArrayHttpMessageConverter());
            messageConverters.add(new StringHttpMessageConverter());
            messageConverters.add(new ResourceHttpMessageConverter());
            messageConverters.add(new ResourceRegionHttpMessageConverter());
            if (!shouldIgnoreXml) {
                try {
                    messageConverters.add(new SourceHttpMessageConverter<>());
                }
                catch (Throwable ex) {
                    // Ignore when no TransformerFactory implementation is available...
                }
            }
            messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    
            if (romePresent) {
                messageConverters.add(new AtomFeedHttpMessageConverter());
                messageConverters.add(new RssChannelHttpMessageConverter());
            }
    
            if (!shouldIgnoreXml) {
                if (jackson2XmlPresent) {
                    Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
                    if (this.applicationContext != null) {
                        builder.applicationContext(this.applicationContext);
                    }
                    messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
                }
                else if (jaxb2Present) {
                    messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
                }
            }
    
            if (kotlinSerializationJsonPresent) {
                messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
            }
            if (jackson2Present) {
                Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
                if (this.applicationContext != null) {
                    builder.applicationContext(this.applicationContext);
                }
                messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
            }
            else if (gsonPresent) {
                messageConverters.add(new GsonHttpMessageConverter());
            }
            else if (jsonbPresent) {
                messageConverters.add(new JsonbHttpMessageConverter());
            }
    
            if (jackson2SmilePresent) {
                Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
                if (this.applicationContext != null) {
                    builder.applicationContext(this.applicationContext);
                }
                messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
            }
            if (jackson2CborPresent) {
                Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
                if (this.applicationContext != null) {
                    builder.applicationContext(this.applicationContext);
                }
                messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
            }
        }
    
    1. MVC在配置消息转换器时,先从容器中获取配置的消息转换器,然后再去添加默认的消息转换器

              //WebMvcAutoConfiguration.class 配置消息转换器
              @Override
              public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
                  this.messageConvertersProvider
                          .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
              }
              //HttpMessageConverters.class
              //1. 从容器中获取配置的HttpMessageConverter (因此,我们可以直接配置自定义的消息转换器)
              public HttpMessageConverters(Collection<HttpMessageConverter<?>> additionalConverters) {
                  this(true, additionalConverters);
              }
              //2.再去添加默认的消息转换器
              public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) {
                  List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
                          addDefaultConverters ? getDefaultConverters() : Collections.emptyList());
                  combined = postProcessConverters(combined);
                  this.converters = Collections.unmodifiableList(combined);
              }
      
      1. 自定义消息转换器

        //必须实现HttpMessageConverter<T>接口,并重写其相关方法 
        @Configuration
        public class MyHttpMessageConverter implements HttpMessageConverter<MyUser> {
        
            ////根据需求去调整
            @Override
            public boolean canRead(Class clazz, MediaType mediaType) {
                return false;
            }
            //根据需求去调整
            @Override
            public boolean canWrite(Class clazz, MediaType mediaType) {
                if (null==mediaType){
                    //判断该返回类型是否是MyUser类型或两者能否相互转换(此种写法只能处理MyUser类型的返回值)
                    return clazz.isAssignableFrom(MyUser.class);
                }
                //判断该媒体类型是否是自定义的媒体类型  当请求头的accept="application/xx-guigu" 时,即可写出
                return mediaType.isCompatibleWith(MediaType.parseMediaType("application/xx-guigu"));
            }
        
            //根据需求去调整
            @Override
            public List<MediaType> getSupportedMediaTypes() {
                List<MediaType> mediaTypes = new ArrayList<>();
                MediaType mediaType = new MediaType(MediaType.parseMediaType("application/xx-guigu"));
                mediaTypes.add(mediaType);
                return mediaTypes;
            }
        
            @Override
            public MyUser read(Class<? extends MyUser> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
                return null;
            }
        
            //根据需求去调整
            @Override
            public void write(MyUser myUser, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                String out = myUser.getUserName()+";"+myUser.getUserAge()+";"+myUser.getPet().getPetName()+";"+myUser.getPet().getPetAge();
                outputMessage.getBody().write(out.getBytes("GBK"));
            }
            ////根据需求去调整
            @Override
            public List<MediaType> getSupportedMediaTypes(Class clazz) {
                List<MediaType> mediaTypes = new ArrayList<>();
                MediaType mediaType = new MediaType(MediaType.parseMediaType("application/xx-guigu"));
                mediaTypes.add(mediaType);
                return mediaTypes;
            }
        }
        

二 内容协商

根据客户端接收能力不同,返回不同媒体类型的数据。

1. 内容协商原理

以下顺序来源AbstractMessageConverterMethodProcessor.class

  1. 获取浏览器支持的返回值类型

        List<MediaType> acceptableTypes;
        try {
           acceptableTypes = getAcceptableMediaTypes(request);
        }
    
  2. 获取服务端能产生的返回值类型

        List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
    
        protected List<MediaType> getProducibleMediaTypes(
            HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
    
            Set<MediaType> mediaTypes =
                (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
            if (!CollectionUtils.isEmpty(mediaTypes)) {
                return new ArrayList<>(mediaTypes);
            }
            List<MediaType> result = new ArrayList<>();
            //从所有消息处理器中 获取能处理当前返回值的处理器
            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));
                }
            }
            return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
        }
    
    1. 对比浏览器能接收的返回值类型和服务端能产生的返回值 类型 ,得出能用的媒体类型MediaType

                  List<MediaType> mediaTypesToUse = new ArrayList<>();
                  for (MediaType requestedType : acceptableTypes) {
                      for (MediaType producibleType : producibleTypes) {
                          if (requestedType.isCompatibleWith(producibleType)) {
                              mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
                          }
                      }
                  }
      
    2. 将能用的媒体类型通过权重排序,最终选出浏览器真正接收的媒体类型

                  MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
      
                  for (MediaType mediaType : mediaTypesToUse) {
                      if (mediaType.isConcrete()) {
                          selectedMediaType = mediaType;
                          break;
                      }
                      else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
                          selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                          break;
                      }
                  }
      
    3. 最后从消息转换器中找到能写出该媒体类型的转换器,进行数据写出

              if (selectedMediaType != null) {
                  selectedMediaType = selectedMediaType.removeQualityValue();
                  //循环遍历消息转换器,找到能处理该媒体类型的转换器
                  for (HttpMessageConverter<?> converter : this.messageConverters) {
                      GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                              (GenericHttpMessageConverter<?>) converter : null);
                      //判断能不能写该类型的消息
                      if (genericConverter != null ?
                              ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                              converter.canWrite(valueType, selectedMediaType)) {
                          body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                                  (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                                  inputMessage, outputMessage);
                          if (body != null) {
                              Object theBody = body;
                              LogFormatUtils.traceDebug(logger, traceOn ->
                                      "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                              addContentDispositionHeader(inputMessage, outputMessage);
                              if (genericConverter != null) {
                                  //写出消息
                                  genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                              }
                              else {
                                  ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                              }
                          }
                          else {
                              if (logger.isDebugEnabled()) {
                                  logger.debug("Nothing to write: null body");
                              }
                          }
                          return;
                      }
                  }
              }
      

2. 引入xml依赖

 <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

3. 测试

引入jackson-dataformat-xml依赖后,使用浏览器访问,后台返回的数据全部变成了xml,使用postman访问,数据依然是json

1. 浏览器:

请求头携带参数

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

在内容协商流程第四步,通过权重排序,选出浏览器最终的接受媒体类型,最后得到的是application/xml类型数据

2.postman

请求头携带参数 Accept:*/*

在内容协商流程第一步,得到客户端能接受任意的返回值参数,第四步,通过权重排序,选出最终的媒体类型,最后得到的是application/json类型数据

结论:浏览器和postman测试的最终返回结果不同,是因为判断客户端接收媒体类型时不同,浏览器是xml,postman是json

4.jackson-dataformat-xml

为什么没有引入jackson-dataformat-xml依赖前,浏览器依然是返回的json,而引入依赖后,浏览器返回却变成了xml

  1. 引入Jackson.xml前,服务端能处理的消息类型,一共有10种,其中没有xml处理的消息转换器image-20220608094236798

  2. 引入jackson.xml后,多出了jackson2xml的消息转换器image-20220608094429745

因此,引入jackson.xml后,获取到的媒体类型有支持xml的,浏览器访问时,通过权重排序会获取到xml的媒体类型,最终使用jackson2xml的消息转换器

5.开启浏览器参数方式内容协商功能

spring:
    mvc:
        contentnegotiation:
              favor-parameter: true
              parameter-name: guigu  #自定义参数名

自定义内容协商参数值:

内容协商功能由ContentNegotiationManager.class管理,通过ContentNegotiationConfigurer进行配置

默认只有两种参数值xml和json(WebMvcConfigurationSupport.class)image-20220608103035758

ContentNegotiationConfigurer中有mediaType方法可以添加mediaType,通过WebMvcConfigurer实现自定义mediaType参数名称和对应的解析值

    public ContentNegotiationConfigurer mediaType(String extension, MediaType mediaType) {
        this.mediaTypes.put(extension, mediaType);
        return this;
    }
    //配置内容协商,添加媒体自定义媒体类型 
    @Bean
    public WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer(){
            @Override
            public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
                MediaType mediaType = new MediaType(MediaType.APPLICATION_JSON);
                configurer.mediaType("test",mediaType);
            }
        };
    }
正文到此结束