由一个序列化框架的更换引发的问题

问题背景

项目使用SpringMVC4.1.X作为web框架,序列化框架选择Jackson。出于使用习惯以及性能考虑,将其切换到了fastjson。配置如下:

 1 <mvc:annotation-driven>
 2         <mvc:message-converters register-defaults="true">
 3             <!-- 避免IE执行AJAX时,返回JSON出现下载文件 -->
 4             <bean id="fastJsonHttpMessageConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
 5                 <property name="supportedMediaTypes">
 6                     <list>
 7                         <value>application/json;charset=UTF-8</value>
 8                         <value>text/html;charset=UTF-8</value>
 9                         <value>application/x-www-form-urlencoded;charset=UTF-8</value>
10                     </list>
11                 </property>
12                 <property name="features">
13                     <list>
14                         <value>WriteMapNullValue</value>
15                         <value>WriteNullStringAsEmpty</value>
16                         <value>WriteNullListAsEmpty</value>
17                         <value>WriteNullNumberAsZero</value>
18                     </list>
19                 </property>
20             </bean>
21         </mvc:message-converters>
22     </mvc:annotation-driven>

 

问题表现

如上配置后,一段时间后,线上出现故障。对接方反馈其请求成功,但是解析响应报文失败。故障表现如下:

如上所示,响应的Content-Type为表单。对接方说之前是application/json。乍看之下有点蒙,更改一个序列化框架会导致响应Content-Type发生变化。

问题原因

仔细看了下其请求header,发觉了有点不太对的地方。其请求的Accpet是application/x-www-form-urlencoded,那我响应的Content-Type是这个就没有问题才对。但之前说这样的请求我们响应的是application/json,那回过头去看之前的配置如下:

 1 <bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
 2         <!-- 扩展名至mimeType的映射,即 /user.json => application/json -->
 3         <property name="favorPathExtension" value="true"/>
 4         <!-- 用于开启 /userinfo/123?format=json 的支持 -->
 5         <property name="favorParameter" value="true"/>
 6         <property name="parameterName" value="format"/>
 7         <!-- 是否忽略Accept Header -->
 8         <property name="ignoreAcceptHeader" value="false"/>
 9         <property name="mediaTypes"> <!--扩展名到MIME的映射;favorPathExtension, favorParameter是true时起作用  -->
10             <value>
11                 json=application/json
12             </value>
13         </property>
14     </bean>
15 
16     <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver" p:order="0">
17         <!-- 内容协商管理器 用于决定media type -->
18         <property name="contentNegotiationManager" ref="contentNegotiationManager"/>
19         <!-- 默认视图 放在解析链最后 -->
20         <property name="defaultViews">
21             <list>
22                 <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
23             </list>
24         </property>
25     </bean>

之前的配置是基于Spring的内容协商机制来实现内容响应控制。按上述配置,会组装出基于一组基于:

  • 路径扩展
  • 参数扩展
  • accept

的内容协商策略。这组策略会按照上述顺序解析请求的媒体类型,如果某个策略可以识别请求的媒体类型,则不再继续后续的识别。如下ContentNegotiationManager代码:

 1     public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
 2         for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
 3             List<MediaType> mediaTypes = strategy.resolveMediaTypes(webRequest);
 4             if (mediaTypes.isEmpty() || mediaTypes.equals(MEDIA_TYPE_ALL)) {
 5                 continue;
 6             }
 7             return mediaTypes;
 8         }
 9         return Collections.emptyList();
10     }

那么为啥之前的配置可以在客户端的accept设置为application/x-www-form-urlencoded时仍然可以返回application/json呢?缘由在于请求参数有一个format=json,匹配了这里的参数扩展策略以及对应的媒体类型mediaTypes中的key。

去掉内容协商后,就从请求header中读取accept来确认响应content-type。

后面调整的配置指定了FastJsonHttpMessageConverter作为第一个转换器。设置其支持的转换媒体类型有:

  • application/json;charset=UTF-8
  • text/html;charset=UTF-8
  • application/x-www-form-urlencoded;charset=UTF-8

其中就有application/x-www-form-urlencoded;charset=UTF-8这种媒体类型。当前端的header中指定accept的类型为此时,后端响应请求做消息转换时,会match消息转换器配置的支持类型与前端要求的响应类型,寻找交集。很不幸的是application/x-www-form-urlencoded;charset=UTF-8正好能匹配,故此时通过FastJsonHttpMessageConverter返回json的数据,然后content-type是application/x-www-form-urlencoded;charset=UTF-8。

这块的逻辑在SpringMVC的AbstractMessageConverterMethodProcessor如下片段:

 1         HttpServletRequest servletRequest = inputMessage.getServletRequest();
 2         List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
 3         List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
 4 
 5         Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
 6         for (MediaType r : requestedMediaTypes) {
 7             for (MediaType p : producibleMediaTypes) {
 8                 if (r.isCompatibleWith(p)) {
 9                     compatibleMediaTypes.add(getMostSpecificMediaType(r, p));
10                 }
11             }
12         }
13         if (compatibleMediaTypes.isEmpty()) {
14             throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
15         }
16 
17         List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
18         MediaType.sortBySpecificityAndQuality(mediaTypes);
19 
20         MediaType selectedMediaType = null;
21         for (MediaType mediaType : mediaTypes) {
22             if (mediaType.isConcrete()) {
23                 selectedMediaType = mediaType;
24                 break;
25             }
26             else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
27                 selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
28                 break;
29             }
30         }
31 
32         if (selectedMediaType != null) {
33             selectedMediaType = selectedMediaType.removeQualityValue();
34             for (HttpMessageConverter<?> messageConverter : messageConverters) {
35                 if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
36                     ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
37                     if (logger.isDebugEnabled()) {
38                         logger.debug("Written [" + returnValue + "] as "" + selectedMediaType + "" using [" +
39                                 messageConverter + "]");
40                     }
41                     return;
42                 }
43             }
44         }

解决方案

如上所述,根本问题是后端响应的content-type设置有一个优先级顺序。优先基于后端策略控制来处理,然后基于前端的请求header的accept来控制。那去掉后端的内容决策逻辑后,响应内容就依赖前端的accpet。故存在响应的content-type是application/x-www-form-urlencoded;的情况。

解决方案有2种方式:

  1. 客户端的声明需要内容时,accept设置正确;
  2. 服务端使用内容决策策略来控制响应格式时,客户端的accpet也不可以随便处理,虽然优先级不一样,但是以防万一调整导致未知失败。
原文地址:https://www.cnblogs.com/asfeixue/p/7682123.html