目录
3. MediaType.sortBySpecificityAndQuality排序机制
八、Spring MVC默认转换器SupportedMediaTypes
一、提要
1. 原消息转换器配置
原项目是以自定义消息转换器Bean的方式将fastjson转换器注入Spring容器:
在该方式下,最终注册的消息转换器列表顺序如下:
JsonFormatUtils.setHttpMessageConverter()中supportedMediaTypes内容:
2. 存在问题
该配置下,xml、String格式内容在转换上会出现问题,带@XmlRootElement注解的实体类无法转换成预期的xml格式,String内容的转换上会按标准的Json格式带上双引号(使用纯String做返回往往并不是期望标准的Json格式)
3. 影响接口
项目中已知的受影响的有使用几个通过xml格式数据交互的接口,主要影响的是返回数据,要求返回的是String格式的xml内容,但是实际返回却被格式化加上了双引号:
其中一个接口是通过mapping参数@PostMapping.produces=”test/plain;charset=UTF-8”解决(text错写成test,特殊字符被编码可通过SerializerFeature.PrettyFormat配置解决):
4. 简析
FastJson转换器通过自定义转换器Bean注入时,会默认设置为第一优先级。在未指定返回内容MediaType的情况下,返回内容默认都会通过FastJson转换器转换成Json格式。
指定MediaType时,通过设置response请求头条的Content Type或mapping注解的produces参数为”text/plain;charset=UTF-8”,然而并不能按预期转换成不带引号String内容。因为FastJson转换器配置里支持转换类型配置了该类型,所以该类型会使用FastJson进行转换。但是FastJson仅支持转换成Json格式,并不能正确转换该类型。
通过配置@PostMapping.produces=”test/plain;charset=UTF-8”之后(test,非text),该类型FastJson转换器未适配该类型,则会继续向下遍历其他转换器,最终由MVC自带的StringHttpMessageConverter转换器进行转换(该转换器兼容了所有Media Type[*/*],只要返回类型[String]校验通过即可适配)。
上面是权宜之计,要彻底解决转换器问题,需要调整转换器列表顺序,并调整FastJson转换器配置参数。将陆续对各个使用FastJson做转换器的服务进行调整。
下面是探索消息转换器加载、适配相关源码的解析。
内容省去部分细节,为基本流程。流程图全程伪代码。
二、消息转换器注册时机
1. 请求消息转换器注册时机
WebMvcConfigurationSupport是MVC配置的主类,其requestMappingHandlerAdapter负责配置返回一个用于通过带注释的控制器方法处理请求的适配器。处理请求的消息转换器在这里被注册,通过adapter.setMessageConverters(getMessageConverters())配置。
getMessageConverters调用链:
通过调用链追溯到底,DefaultSingletonBeanRegistry.singletonObjects存储着所有注入的单例bean:
消息转换器列表早在配置请求适配器之前已经实例化完成并存储在这里:
该适配器仅仅是将消息转换器取出并指定他们来处理请求消息的转换。
三、Media Type
请求返回消息的Media Type处理、消息转换器的适配主要发生在方法AbstractMessageConverterMethodProcessor#writeWithMessageConverters中,后面内容都基于该方法。
1. 自定义Media Type生效时机
在返回消息进行消息转换之前,会进行Media Type的适配,决定最终要转换成的Media Type。
(1)response请求头设置Content Type
尝试从ServletServerHttpResponse的请求头中获取Content Type,如果成功获取到就将其作为目标Media Type。
(2)RequestMapping注解配置参数produces
分两步走。
第一步,请求进来时金入目标方法之前,会执行RequestMappingInfoHandlerMapping的handleMatch方法进行一次适配,当目标方法的RequestMapping注解参数produces有值时,将以常量PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE作为key往当前session缓存produces。
第二步,目标方法执行结束后执行消息转换之前,若response请求头未获取到Content Type,则会从session中获取缓存的produces,若不为空则将其作为目标Media Type。
2. 默认Media Type
当目标方法未自定义Media Type时,将会按消息转换器优先级取出所有可以转换返回类型的消息转换器所有支持转换的Media Type,再通过MediaType.sortBySpecificityAndQuality进行排序,并取出排在第一位的Media Type作为默认Media Type。
所以,在未定义返回消息Media Type的时候,消息转换器的优先级对使用的Media Type有直接影响。
3. MediaType.sortBySpecificityAndQuality排序机制
该方法主要根据Media Type的特性和权重做标准进行排序。MVC框架中未对Media Type分配权重,实际开发中也几乎不需要配置权重,所以Media Type权重基本都是默认1。
在排序开始前,若传入的Media Type个数不大于1则不进行排序。排序时先使用MediaType类的比较器常量SPECIFICITY_COMPARATOR,比较相等的情况下再调用比较器常量
QUALITY_VALUE_COMPARATOR。
SPECIFICITY_COMPARATOR比较器优先比较Media Type的权重,权重高的往前排。权重相等的情况下比较携带参数的个数,参数个数多的往前排。如application/json和application/json;charset=UTF-8,两者携带参数个数分别为0、1,后者优先级会更高。
QUALITY_VALUE_COMPARATOR比较器以同样方式优先比较权重。权重相等的情况下比较Media Type内容,需要分别比较主类别和子类别(application/json,application为主类别,json为子类别)。
QUALITY_VALUE_COMPARATOR比较分以下几种情况:
(1)主类别不包含通配符的情况下,主类别不相等的情况下,优先级相等(audio/basic = text/html = application/*)
(2)子类别不包含通配符的情况下,主类别相等子类别不相等的情况下,优先级相等(*/json = */html;text/html = text/plain)
(3)主类别和子类别都相等的情况下比较参数个数,参数个数多的优先级就高(application/json < application/json;charset=UTF-8)
(4)通配符*优先级最低(*/* < audio/*;audio/* < audio/basic)
由上可见,除去权重不说,SPECIFICITY_COMPARATOR先将参数多的类型前置,QUALITY_VALUE_COMPARATOR再根据通配符优先级低、参数多的优先级高的设定,按照类型进行分组排序。
四、消息转换器优先级
1. 转换器匹配规则
消息转换器通过实现HttpMessageConverter或GenericHttpMessageConverter(继承于前者,重载了canWrite方法)接口类的canWrite方法,实现匹配规则逻辑。
一般方法内做两种判断:是否支持转换传入的Class和Media Type。是否支持转换传入的Class一般通过实现抽象类的抽象接口supports实现。
Json转换器一般只要是对象都支持转换,下面看几个转换器的supports是怎么实现的:
(1)FastJsonHttpMessageConverter的supports直接返回true:
(2)MappingJackson2HttpMessageConverter继承AbstractJackson2HttpMessageConverter的supports方法,直接返回true:
(3)StringHttpMessageConverter仅支持转换String:
(4)Jaxb2RootElementHttpMessageConverter的supports逻辑直接写在canWrite方法中,仅支持对带@XmlRootElement注解的对象进行转换:
2. 原则优先级
canWrite方法主要在获取默认Media Type和最后选择要用的消息转换器会被用到。
通过前面canWrite中supports逻辑的分析知,在没有指定Media Type,需要获取默认Media Type列表的时候,Json转换器的Media Type无论如何都会被获取到。在匹配转换器的时候,只要Json转换器前面的转换器没有被匹配到,Json转换器就一定会被匹配到。
也就是说,排在Json转换器后面的转换器就一定会失效,除非指定Json转换器不能适配的Media Type。
所以,为保证MVC其他的转换器正常运作,原则上Json转换器的优先级应该是最低的。
五、请求消息转换适配基本流程
基于AbstractMessageConverterMethodProcessor类:
六、解决方法
经分析,解决消息转换器问题需要调整FastJson转换器入场时机,调整转换器列表顺序将Json转换器的优先级降到最低。FastJson转换器的supportedMediaTypes配置需要调整为仅支持Json消息转换。
WebMvcConfigurer配置:
七、问题与误区
1. FastJson消息转换器存在问题,不能正确解析除Json外的数据格式
FastJson是Json消息转换器,本身仅支持Json消息转换,不支持其他格式的转换。
原项目中FastJson以消息转换器列表Bean的方式注入,会将FastJson注册到消息转换器列表的第一位,在未指定Media Type的情况下所有返回都会走FastJson转换器,打成标准Json格式,其他消息转换器会全部失效。所以这个锅FastJson不背,是FastJson入场时机不对导致转换器优先级分布不合理造成的。
而指定正确Content Type和RequestMapping.produces无效,是因为配置中存在误区。supportedMediaTypes是配置可以使用该转换器的Media Type,并非赋予转换器转换该类型的能力。原项目supportedMediaTypes指定了text/plain,所以就算指定了produces为text/plain,依旧会适配到FastJson转换器,导致转换出来的结果错误。
2. FastJson转换器与Jackson转换器共存造成冲突,导致转换失效
消息转换器顺序遍历适配,不存在冲突。失效原因同1。
3. 移除FastJson转换器,使用默认转换器就不存在问题
默认转换器列表顺序:
可见MappingJackson2HttpMessageConverter在Jaxb2RootElementHttpMessageConverter之前,所以后者失效(验证过)。后者是用来转换带@XmlRootElement注解的对象,转换成标准XML内容格式(带xml头部),一般项目中很少会用到:
同时可以发现,MappingJackson2HttpMessageConverter重复注册了两次。
所以默认装换器列表还是存在问题,只是影响甚微。
业务方面,如果原先项目中是FastJson转换器生效,Json序列化访问权控制、格式转换等都是使用@JSONField,如果拿掉FastJson影响范围将是特别广。
所以,如果项目中已经基于FastJson转换器做了大量的开发,暂时是不建议拿掉FastJson转换器的,代价太大。