![d40c70c1c9cf4ea761b855d10b5ddc3e.png](https://img-blog.csdnimg.cn/img_convert/d40c70c1c9cf4ea761b855d10b5ddc3e.png)
背景
在后端定义接口的时候,后端接口返回一般都是Content-Type=application/json
格式的json串。
最近碰到一个问题,负责的一个系统在升级组里的一个公共依赖包common
的版本之后,所有的接口返回的Content-Type
都由application/json
变成了text/html
,导致接收端调用解析报错。
把版本修改回去则接口返回的Content-Type
即又恢复正常。
排查
这个问题的排查思路其实很简单,首先查看common
包做了什么变动。
common
包中只增加了一个配置类:
@Configuration
public class JacksonConfig {
@Bean
@ConditionalOnMissingBean(ObjectMapper.class)
public ObjectMapper objectMapper() {
return JacksonUtils.objectMapper().propsIgnorable().build();
}
@Bean
@ConditionalOnMissingBean(JsonViewSupportFactoryBean.class)
public JsonViewSupportFactoryBean views() {
return new JsonViewSupportFactoryBean(objectMapper());
}
}
初看过去觉得没啥问题,不知道为什么增加这么一个配置类就会影响接口返回的格式。
那么就换一个思路,来看看SpringBoot框架是怎么判断接口最终应该返回什么格式的Content-Type
。
通过对SpringBoot框架源码调试,最终发现SpringBoot框架是在AbstractMessageConverterMethodProcessor
类中的writeWithMessageConverters()
方法中实现判断返回格式的。
(如果不知道怎么找到这个地方,可以在Controller接口的最后一句加断点,等程序执行到断点处在F7快捷键Step Into,进入到框架源码内部调试)
@SuppressWarnings({"rawtypes", "unchecked"})
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
// ... 部分省略代码
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();
// 获取调用方能接受什么类型的MediaType
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 获取服务提供方能产生哪些类型的MediaType
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
// 综合请求方和服务提供方的MediaType情况,计算最终能够返回哪些MediaType
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进行排序
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
// 计算最终选择返回哪个MediaType,按照先后顺序,只要有一个符合条件,则直接返回,忽略剩余其他的可满足条件的MediaType
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);
}
}
// ...其他省略代码,包括最终的结果的返回
}
writeWithMessageConverters()
方法主要作用就是把接口返回的结果经过合适的Converter
处理之后再返回。
这就要求首先判断应该返回什么类型的MediaType
。
writeWithMessageConverters()
方法判断使用什么类型的MediaType
逻辑如下:
首先调用getAcceptableMediaTypes(request)
判断接收方能接受哪些类型的MediaType
。
如果没有设置的话,则按照默认的来。默认的MediaType
为MEDIA_TYPE_ALL_LIST
List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);
/**
* Public constant media type that includes all media ranges (i.e. "*/*").
*/
public static final MediaType ALL;
/**
* A String equivalent of {@link MediaType#ALL}.
*/
public static final String ALL_VALUE = "*/*";
接着调用getProducibleMediaTypes()
方法来计算当前接口能产生哪些类型的MediaType
。
/**
* Returns the media types that can be produced. The resulting media types are:
* <ul>
* <li>The producible media types specified in the request mappings, or
* <li>Media types of configured converters that can write the specific return value, or
* <li>{@link MediaType#ALL}
* </ul>
* @since 4.2
*/
@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
// 如果request中已经指定了MediaType,则直接使用指定的
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<>();
// 依次遍历当前系统中所有的HttpMessageConverte列表,只要能够支持写入指定的targetType,即认为可生成converter支持的MediaType
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
getProducibleMediaTypes()
方法首先判断request中有没有指定特定的MediaType
,如果有的话则直接使用指定的,如果没有的话,则依次遍历当前系统中所有的HttpMessageConverter
,只要对应Converter
的canWrite()
方法返回true,则把对应Converter
所支持的所有的MediaType
加入返回列表中。
当前系统中支的所有HttpMessageConverter
列表如下:
![f4bea53a788f2a51d5cf2353894800f9.png](https://img-blog.csdnimg.cn/img_convert/f4bea53a788f2a51d5cf2353894800f9.png)
执行完成发现第一个canWrite()
返回true的Converter
是BczRequestConfig$HtmlJsonMessageConverter
。
执行完成之后的 result
的结果如下图所示:
![0040315d8f3d88fed24cd148dbfdad8e.png](https://img-blog.csdnimg.cn/img_convert/0040315d8f3d88fed24cd148dbfdad8e.png)
在计算得到所有可以生成的MediaType
之后,又会依次判断这些可以生成的MediaType
是否兼容acceptableTypes
,由于本次请求中acceptableTypes
为默认值,则默认兼容。
之后会把上一步中得到的所有的MediaType
按照各自的qualityValue(每个MediaType都会有一个值)
进行从小到大排序。
本系统中没有做任何特殊的设置,默认值都是1,所有MediaType
顺序保持不变。
做完上述操作之后,从上一步中处理之后的所有MediaType
中选择第一个确定的MediaType
(所谓的确定的MediaType
是指该MediaType
对应的type
和subtype
都是具体的,不存在通配符的)作为该次请求应该返回的MediaType
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
位于列表中第一个的MediaType
为text/html
,符合条件,所以text/html
即被认为只接口请求最终返回的MediaType
。
现在接口返回的Content-Type
格式变成了text/html
的原因已经找到了,那么为什么升级common
包引入JacksonConfig
配置类之后就会变成这样呢?
来看看升级版本之前的MediaType
:
![26769e7647b3404eb2ba3b82dd92e68d.png](https://img-blog.csdnimg.cn/img_convert/26769e7647b3404eb2ba3b82dd92e68d.png)
可以看到引入JacksonConfig
之前,HttpMessageConverter
列表中排在BczRequestConfig$HtmlJsonMessageConverter
前面有两个MappingJackson2HttpMessageConverter
,计算出的可以生成的MediaType
集合中排在第一个的是application/json
,所以接口返回的是Content-Type
为JSONapplication/json
。
现在重新来看看JacksonConfig
:
@Bean
@ConditionalOnMissingBean(JsonViewSupportFactoryBean.class)
public JsonViewSupportFactoryBean views() {
return new JsonViewSupportFactoryBean(objectMapper()); // 在全局objectMapper的基础上通过convert修改
}
JacksonConfig
主要就是生成一个工厂BeanJsonViewSupportFactoryBean
对象,进入到JsonViewSupportFactoryBean
类源码中,发现JsonViewSupportFactoryBean
实现了InitializingBean
接口,
@Override
public void afterPropertiesSet() throws Exception {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(adapter.getReturnValueHandlers());
// 把当前系统中已配置的JacksonConverter全部删掉,然后再把新生成的JsonViewMessageConverter加入到转换器列表的末尾
List<HttpMessageConverter<?>> converters = removeJacksonConverters(adapter.getMessageConverters());
converters.add(converter);
adapter.setMessageConverters(converters);
decorateHandlers(handlers);
adapter.setReturnValueHandlers(handlers);
}
removeJacksonConverters()
方法实现如下:
protected List<HttpMessageConverter<?>> removeJacksonConverters(List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> copy = new ArrayList<>(converters);
Iterator<HttpMessageConverter<?>> iter = copy.iterator();
while(iter.hasNext()) {
HttpMessageConverter<?> next = iter.next();
// 把所有类名义MappingJackson2开始的转换前全部删掉
if (next.getClass().getSimpleName().startsWith("MappingJackson2")) {
log.debug("Removing {} as it interferes with us", next.getClass().getName());
iter.remove();
}
}
return copy;
}
removeJacksonConverters()
方法会把当前所有类名以MappingJackson2
的Converter
全部删掉。
执行删除前后的转换器列表如下:
![ad5d6e315cbe3a126909a45cfb255cf5.png](https://img-blog.csdnimg.cn/img_convert/ad5d6e315cbe3a126909a45cfb255cf5.png)
综上:
升级common
版本引入JacksonConfig
类之后,JacksonConfig
中引入了JsonViewSupportFactoryBean
工厂对象,JsonViewSupportFactoryBean
对象的afterPropertiesSet()
方法中会把当前系统中所有类名以MappingJackson2
开头的Converter
删掉,然后在列表最末尾增加JsonViewMessageConverter
。
SpringBoot框架在计算接口返回MediaType
的时候会选择第一个符合要求的MediaType
。由于JsonViewSupportFactoryBean
打乱了HttpMessageConverter
列表的顺序,导致text/html
格式的MediaType
排在application/json
的前面,所以最终接口返回变成了text/html
。
解决
不通过JsonViewSupportFactoryBean
引入JsonViewMessageConverter
,改成手动生成,且JsonViewMessageConverter
排在列表最前面即可。
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new JsonViewMessageConverter(mObjectMapper));
converters.add(new HtmlJsonMessageConverter(mObjectMapper));
}