@ResponseBody不一定返回json

先说结论

  1. @requestmapping不指定produces注解值的情况下,@ResponseBody不一定返回json,而是根据request请求头的accept类型,根据权重返回
  2. @requestmapping指定produces的情况下,reponse会设置reponse body的header为对应类型,不在根据请求接受的类型返回数据

csdn :@ResponseBody并不是以json返回。不加@ResponseBody,是将方法返回的值作为视图名称,并自动匹配视图去显示,而加上@ResponseBody就仅仅是将方法返回值当作内容直接返回到客户端,并且会自适应响应头的content-type,返回的字符串符合json,那么content-type就是application/json,如果是普通字符串,就是text/plain,但是加上注解属性produces=application/json,那么不管内容是什么格式,响应头的content-type就一直是application/json,不再去做自适应,至于内容是不是json都不重要了

背景

  • @ResponseBody 默认情况返回的数据格式是什么?所谓默认情况 后台接口不指定 produces MediaType
@Controller
public class DemoController {
  @ResponseBody
  @GetMapping(value = "/demo")
  public DemoVO demo() {
    return new DemoVO("lengleng", "123456");
  }
}
  • 使用百度搜索 @ResponseBody 排名第一的答案, @ResponseBody 的作用其实是将 java 对象转为 json 格式的数据。
image.png

正确答案

我们先来公布正确的答案。

@ResponseBody 的输出格式,默认情况取决于客户端的 Accept 请求头。

image.png
image.png

源码剖析

  • 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 模块 引入以下依赖
image.png
  • 当依赖中出现 dataformat jar 时候, RestTemplate ,会在默认 Accept 请求头增加

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

image.png
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 ,导致不能平滑升级

相同的问题,参考:

FeignClient: no suitable HttpMessageConverter for application/octet-stream 问题解决
https://www.jianshu.com/p/54b45e4fdcf3
因为feignclient调用时,未指定accept-type的类型,导致接口返回的数据格式不是json,而是application/octet-stream类型,也就是未知二进制类型,导致feignclient无法解析消息体。

HTTP报文头Accept和Content-Type总结

1.Accept属于请求头, Content-Type属于实体头。

Http报头分为通用报头,请求报头,响应报头和实体报头。
请求方的http报头结构:通用报头|请求报头|实体报头
响应方的http报头结构:通用报头|响应报头|实体报头

2.Accept代表发送端(客户端)希望接受的数据类型。

比如:Accept:text/xml(application/json);
代表客户端希望接受的数据类型是xml(json )类型
Content-Type代表发送端(客户端|服务器)发送的实体数据的数据类型。

比如:Content-Type:text/html(application/json) ;
代表发送端发送的数据格式是html(json)。
二者合起来,

Accept:text/xml;
Content-Type:text/html

即代表希望接受的数据类型是xml格式,本次请求发送的数据的数据格式是html。

如果accept指定的类型和response返回的类型不一致,会出现406,not acceptable错误,对应到java spring工程,就是

HttpHeaders headers = new HttpHeaders();

headers.add("Accept", MediaType.APPLICATION_JSON_UTF8_VALUE.toString());

如果head头里加了accept,那么@RequestMapping里的produces = "*/*"produces = "application/json"

Conteny-Type:内容类型,即请求/响应的内容区数据的媒体类型
Accept:用来指定什么媒体类型的响应是可接受的,即告诉服务器我需要什么媒体类型的数据,此时服务器应该根据Accept请求头生产指定媒体类型的数据。
问题:

服务器端可以通过指定【headers = “Content-Type=application/json”】来声明可处理(可消费)的媒体类型,即只消费Content-Type指定的请求内容体数据;

客户端如何告诉服务器端它只消费什么媒体类型的数据呢?即客户端接受(需要)什么类型的数据呢?服务器应该生产什么类型的数据?此时我们可以请求的Accept请求头来实现这个功能。

@RequestMapping(value = "/response/ContentType", headers = "Accept=application/json")
public void response2(HttpServletResponse response) throws IOException {
    //表示响应的内容区数据的媒体类型为json格式,且编码为utf-8(客户端应该以utf-8解码)
    response.setContentType("application/json;charset=utf-8");
    //写出响应体内容
    String jsonData = "{\"username\":\"zhang\", \"password\":\"123\"}";
    response.getWriter().write(jsonData);
}
@Controller    
@RequestMapping(value = "/users", method = RequestMethod.POST, consumes="application/json", produces="application/json")    
@ResponseBody  
public List<User> addUser(@RequestBody User userl) {        
    // implementation omitted    
    return List<User> users;  
}

上面两个例子都表示了request请求中Accept头中包含了"application/json"的请求,同时暗示了返回的内容类型为application/json:

produces标识:produces="application/json"
headers = "Accept=application/json"
其中request Content-Type为“application/json”类型的请求.

当你有如下Accept头,将遵守如下规则进行应用:

Accept:text/html,application/xml,application/json
将按照如下顺序进行produces的匹配 ①text/html ②application/xml ③application/json
Accept:application/xml;q=0.5,application/json;q=0.9,text/html
将按照如下顺序进行produces的匹配 ①text/html ②application/json ③application/xml
参数为媒体类型的质量因子,越大则优先权越高(从0到1)
Accept:*/*,text/*,text/html
将按照如下顺序进行produces的匹配 ①text/html ②text/* ③/
即匹配规则为:最明确的优先匹配。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容