irpas技术客

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

irpas 4922

问题

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

思路

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

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

验证 设置为application/json, 如下图所示, 而且浏览器会优化json的显示 加字符集, 设置为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的主要步骤 先检查Response中Content-Type是否存在或者是明确的, 两者都满足直接赋值给selectedMediaType, 这里通常情况下是空的。获取请求能接受的类型, 这里指的是请求头中的Accept的值, 如图 获取可以生成的Media类型 List<MediaType> getProducibleMediaTypes( HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) 遍历可生成的类型(producibleTypes)和可以接受的类型(acceptableTypes), 判断producibleTypes与acceptableTypes是否兼容, 如果兼容, 将更明确的类型添加到mediaTypesToUse中.对要使用的类型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中设置.


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #Spring