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


    问题背景

    项目使用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也不可以随便处理,虽然优先级不一样,但是以防万一调整导致未知失败。
  • 相关阅读:
    C# 关于爬取网站数据遇到csrf-token的分析与解决
    Nginx实现同一端口HTTP跳转HTTPS
    Console也要美颜了,来给Console添色彩
    程序员如何巧用Excel提高工作效率
    LeetCode每日一练(1-3)
    Json对象转Ts类
    JcApiHelper 简单好用的.Net ApiHelper
    .Net Core Mvc/WebApi 返回结果封装
    C#光盘刻录
    Orm框架开发之NewExpression合并问题
  • 原文地址:https://www.cnblogs.com/asfeixue/p/7682123.html
Copyright © 2020-2023  润新知