SpringMVC中如何设置响应的Content-Type(源码分析)
Spring MVC设置响应头的Content-Type源码.
问题
写这篇文章源于笔者在一次调试接口的时候遇到的一个问题: 在浏览器中调用接口,页面显示的内容中有乱码, 但是查看响应中的内容是没有乱码的, 而且在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中设置.
更多推荐
所有评论(0)