问题

写这篇文章源于笔者在一次调试接口的时候遇到的一个问题: 在浏览器中调用接口,页面显示的内容中有乱码, 但是查看响应中的内容是没有乱码的, 而且在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);
   }
}

根据代码得知

选取Content-Type的主要步骤
  1. 先检查Response中Content-Type是否存在或者是明确的, 两者都满足直接赋值给selectedMediaType, 这里通常情况下是空的。
  2. 获取请求能接受的类型, 这里指的是请求头中的Accept的值, 如图
    在这里插入图片描述
  3. 获取可以生成的Media类型
List<MediaType> getProducibleMediaTypes(
      HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType)
  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));
   }
}

通过源码得知, 排序的主要规则是先通过明确性来排序, 因为类型里有通配符, 没有通配符的比有通配符的更明确, 所以要排在前面. 再通过质量进行排序, 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);
}

在这里插入图片描述
这两个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);
      }
   }
}  

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

fastJsonConverter.setDefaultCharset(StandardCharsets.UTF_8);

在这里插入图片描述
在这里插入图片描述
这里调用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;
}

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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐