天天看點

關于@ResponseBody 預設輸出的誤區

背景

  • @ResponseBody 預設情況傳回的資料格式是什麼?所謂預設情況 背景接口不指定 produces MediaType
@Controller
public class DemoController {
  @ResponseBody
  @GetMapping(value = "/demo")
  public DemoVO demo() {
    return new DemoVO("lengleng", "123456");
  }
}           
  • 使用百度搜尋 @ResponseBody 排名第一的答案, @ResponseBody 的作用其實是将 java 對象轉為 json 格式的資料。
關于@ResponseBody 預設輸出的誤區

正确答案

我們先來公布正确的答案。

@ResponseBody 的輸出格式,預設情況取決于用戶端的

Accept

請求頭。

關于@ResponseBody 預設輸出的誤區
關于@ResponseBody 預設輸出的誤區

源碼剖析

  • RequestResponseBodyMethodProcessor
public class RequestResponseBodyMethodProcessor {
// 處理 ResponseBody 标注的方法
@Override
public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
        returnType.hasMethodAnnotation(ResponseBody.class));
  }
// 處理傳回值
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
                              ModelAndViewContainer mavContainer, NativeWebRequest webRequest) {
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    // 處理傳回值
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
  }
}           
  • writeWithMessageConverters
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
                        ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
  HttpServletRequest request = inputMessage.getServletRequest();
  // 擷取請求頭中的目标資源類型
  List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
  // 擷取接口指定支援的資源類型
  List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
  // 擷取能夠輸出資源類型
  List<MediaType> mediaTypesToUse = new ArrayList<>();
  for (MediaType requestedType : acceptableTypes) {
    for (MediaType producibleType : producibleTypes) {
      if (requestedType.isCompatibleWith(producibleType)) {
        mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
      }
    }
  }
  /// 排序
  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;
    }
  }

  selectedMediaType = selectedMediaType.removeQualityValue();
  // 查找支援選中資源類型的 HttpMessageConverter,輸出body
  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);
      return;
    }
  }
}           

為什麼我要去研究這個問題

  • 當更新至

    spring cloud alibaba 2.2.1

    時, sentinel 子產品 引入以下依賴
關于@ResponseBody 預設輸出的誤區
  • 當依賴中出現 dataformat jar 時候, RestTemplate ,會在預設 Accept 請求頭增加

application/xml | text/xml | application/*+xml

關于@ResponseBody 預設輸出的誤區
public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) {
  super(objectMapper, new MediaType("application", "xml", StandardCharsets.UTF_8),
      new MediaType("text", "xml", StandardCharsets.UTF_8),
      new MediaType("application", "*+xml", StandardCharsets.UTF_8));
  Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required");
}           
  • 當我們使用 RestTemplate 調用接口時候,若不指定 Accept 會傳回 XML ,導緻不能平滑更新