SpringMVC中如何设置响应的Content-Type(源码分析)

发布时间 2024-01-08 20:05:56作者: l_v_y_forever
转载自:https://blog.csdn.net/CaptHua/article/details/122004067
问题

写这篇文章源于笔者在一次调试接口的时候遇到的一个问题: 在浏览器中调用接口,页面显示的内容中有乱码, 但是查看响应中的内容是没有乱码的, 而且在Postman中调用返回的结果正常.

思路

遇到这种情况首先就会想到是不是检查Response, 对比浏览器和Postman中的Response发现, 浏览器响应头中的Content-Type值为text/html, Postman中的为application/json.
在这里插入图片描述
在这里插入图片描述
如果将该值改为application/json或者改为text/html;charset=UTF-8是不是就可以正常显示了?

笔者抱着试一试的心态调试代码, 在 DispatcherServlet.doDispatch() 中打断点, 一路跟代码到
AbstractMessageConverterMethodProcessorwriteWithMessageConverters方法中.
手动将该值改为上述猜想的两种值, 发现果然显示正常.

验证
  1. 设置为application/json, 如下图所示, 而且浏览器会优化json的显示
    在这里插入图片描述
    在这里插入图片描述
  2. 加字符集, 设置为text/html;charset=UTF-8, 如下图所示
    在这里插入图片描述
    在这里插入图片描述

那么新问题来了, 同样的请求为啥浏览器中的响应和Postman中的不一样? 答案肯定是请求不一样, url一样不代表整个请求一样. 继续对比请求发现: 请求头中的Accept是不同的.
在这里插入图片描述
在这里插入图片描述
至此真相大白, 响应头的Content-Type是由请求头的Accept决定的.
那么到底是怎么决定的?是什么关系呢?

源码分析

跟代码到 AbstractMessageConverterMethodProcessor.writeWithMessageConverters() 方法中

MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
   if (logger.isDebugEnabled()) {
      logger.debug("Found 'Content-Type:" + contentType + "' in response");
   }
   selectedMediaType = contentType;
}
else {
   HttpServletRequest request = inputMessage.getServletRequest();
   List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
   List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

   if (body != null && producibleTypes.isEmpty()) {
      throw new HttpMessageNotWritableException(
            "No converter found for return value of type: " + valueType);
   }
   List<MediaType> mediaTypesToUse = new ArrayList<>();
   for (MediaType requestedType : acceptableTypes) {
      for (MediaType producibleType : producibleTypes) {
         if (requestedType.isCompatibleWith(producibleType)) {
            mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
         }
      }
   }
   if (mediaTypesToUse.isEmpty()) {
      if (body != null) {
         throw new HttpMediaTypeNotAcceptableException(producibleTypes);
      }
      if (logger.isDebugEnabled()) {
         logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
      }
      return;
   }

   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;
      }
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Using '" + selectedMediaType + "', given " +
            acceptableTypes + " and supported " + producibleTypes);
   }
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

根据代码得知

选取Content-Type的主要步骤
  1. 先检查Response中Content-Type是否存在或者是明确的, 两者都满足直接赋值给selectedMediaType, 这里通常情况下是空的。
  2. 获取请求能接受的类型, 这里指的是请求头中的Accept的值, 如图
    在这里插入图片描述
  3. 获取可以生成的Media类型
List<MediaType> getProducibleMediaTypes(
      HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType)
 
  • 1
  • 2
  1. 遍历可生成的类型(producibleTypes)和可以接受的类型(acceptableTypes), 判断producibleTypes与acceptableTypes是否兼容, 如果兼容, 将更明确的类型添加到mediaTypesToUse中.
  2. 对要使用的类型mediaTypesToUse进行排序, 然后遍历, 只要是明确的就赋值给选中的类型selectedMediaType。
    这里的排序规则比较重要
public static void sortBySpecificityAndQuality(List<MediaType> mediaTypes) {
   Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
   if (mediaTypes.size() > 1) {
      mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR.thenComparing(MediaType.QUALITY_VALUE_COMPARATOR));
   }
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

通过源码得知, 排序的主要规则是先通过明确性来排序, 因为类型里有通配符, 没有通配符的比有通配符的更明确, 所以要排在前面. 再通过质量进行排序, q大的会排在前面, q如果没有, 默认是1D.

mediaTypes.sort(MediaType.SPECIFICITY_COMPARATOR.thenComparing(MediaType.QUALITY_VALUE_COMPARATOR));
public double getQualityValue() {
   String qualityFactor = getParameter(PARAM_QUALITY_FACTOR);
   return (qualityFactor != null ? Double.parseDouble(unquote(qualityFactor)) : 1D);
}
 
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
这两个COMPARATOR源码可自行查看.

开始时说Content-Type中charset的值也影响显示, 那这个是怎么设置的呢?
继续看源码

AbstractHttpMessageConverter
protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException {
   if (headers.getContentType() == null) {
      MediaType contentTypeToUse = contentType;
      if (contentType == null || !contentType.isConcrete()) {
         contentTypeToUse = getDefaultContentType(t);
      }
      else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
         MediaType mediaType = getDefaultContentType(t);
         contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
      }
      if (contentTypeToUse != null) {
         if (contentTypeToUse.getCharset() == null) {
            Charset defaultCharset = getDefaultCharset();
            if (defaultCharset != null) {
               contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
            }
         }
         headers.setContentType(contentTypeToUse);
      }
   }
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

通过源码得知: ContentType中的charset是获取的HttpMessageConverter中的DefaultCharset().
所以设置converter中的defaultCharset也可以生效.

fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
 
  • 1

在这里插入图片描述
在这里插入图片描述
这里调用servletResponse的addHeader, 最终会调用Response的setCharacterEncoding.

public void setCharacterEncoding(String characterEncoding) throws UnsupportedEncodingException {
    if (isCommitted()) {
        return;
    }
    if (characterEncoding == null) {
        return;
    }

    this.charset = B2CConverter.getCharset(characterEncoding);
    this.characterEncoding = characterEncoding;
}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

设置contentType时是会检查响应是否已提交, 所以在
javax.servlet.Filter的doFilter() 设置contentType是不起作用的, 因为此时响应已经提交. 如果要设置response的header可以在controller中设置.