如下内容,若有问题,烦请指出,谢谢!html
SpringMVC启动时会自动配置一些HttpMessageConverter,接收到http请求时,从这些Converters中选择一个符合条件的来进行Http序列化/反序列化。在不覆盖默认的HttpMessageConverters的状况下,咱们添加的Converter可能会与默认的产生冲突,在某些场景中出现不符合预期的状况。java
在上一篇文章的末尾已经列举了一个jsonConverter冲突的状况:添加一个最低优先级的FastJsonConverter后会有两个(实际上三个,有两个jackson的)jsonConverter,直接使用浏览器访问接口时使用的倒是低优先级的FastJsonConverter来进行序列化操做。git
为了解决converters之间的冲突,或者直接叫优先级问题,须要弄懂SpringMVC是如何选择一个HttpMessageMessagerConverter来进行Http序列化/反序列化的。这篇文章主要就根据相关的代码来说解SpringMVC的这个内部流程,这块的逻辑比较清晰,贴贴代码就基本上都明白了。github
首先须要了解一些HTTP的基本知识(不是强制的而是一种建议与约定):web
一、决定resp.body的Content-Type的第一要素是对应的req.headers.Accept属性的值,又叫作MediaType。若是服务端支持这个Accept,那么应该按照这个Accept来肯定返回resp.body对应的格式,同时把resp.headers.Content-Type设置成本身支持的符合那个Accept的MediaType。服务端不支持Accept指定的任何MediaType时,应该返回错误406 Not Acceptable.
例如:req.headers.Accept = text/html,服务端支持的话应该让resp.headers.Content-Type = text/html,而且resp.body按照html格式返回。
例如:req.headers.Accept = text/asdfg,服务端不支持这种MediaType,应该返回406 Not Acceptable。spring
二、若是Accept指定了多个MediaType,而且服务端也支持多个MediaType,那么Accept应该同时指定各个MediaType的QualityValue,也就是q值,服务端根据q值的大小来决定这几个MediaType类型的优先级,通常是大的优先。q值不指定时,默认视为q=1.
Chrome的默认请求的Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,表示服务端在支持的状况下应该优先返回text/html,其次是application/xhtml+xml.
前面几个都不支持时,服务器能够自行处理 */*,返回一种服务器本身支持的格式。json
三、一个HTTP请求没有指定Accept,默认视为指定 Accept: */*;没有指定Content-Type,默认视为 null,就是没有。固然,服务端能够根据本身的须要改变默认值。浏览器
四、Content-Type必须是具体肯定的类型,不能包含 *.springboot
SpringMvc基本遵循上面这几点。服务器
而后是启动时默认加载的Converter。在mvc启动时默认会加载下面的几种HttpMessageConverter,相关代码在 org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport中的addDefaultHttpMessageConverters方法中,代码以下。
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); stringConverter.setWriteAcceptCharset(false); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new SourceHttpMessageConverter<Source>()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter( Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build())); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { messageConverters.add(new MappingJackson2HttpMessageConverter( Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build())); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } }
这段代码后面还有两个别的处理,一次是将Jaxb放在list最后面,第二次是将一个StringConverter和一个JacksonConverter添加到list中,因此打印出converter信息中这两个有重复的(第二次的那两个来自springboot-autoconfigure.web,重复了不影响后面的流程)。
接着咱们在本身的MVC配置类覆盖extendMessageConverters方法,使用converter.add(xxx)加上上次自定义Java序列化的那个的和FastJson的(把本身添加的放在优先级低的位置)。最后的converters按顺序展现以下(下面的已经去掉重复的StringHttpMessageConverter和MappingJackson2HttpMessageConverter,后续的相应MediaType也去重)
| 类名 | 支持的JavaType | 支持的MediaType |
|-|-|-|
| ByteArrayHttpMessageConverter | byte[] | application/octet-stream, */* |
| StringHttpMessageConverter | String | text/plain, */* |
| ResourceHttpMessageConverter | Resource | */* |
| SourceHttpMessageConverter | Source | application/xml, text/xml, application/*+xml |
| AllEncompassingFormHttpMessageConverter | Map<K, List<?>> | application/x-www-form-urlencoded, multipart/form-data |
| MappingJackson2HttpMessageConverter | Object | application/json, application/*+json |
| Jaxb2RootElementHttpMessageConverter | Object | application/xml, text/xml, application/*+xml |
| JavaSerializationConverter | Serializable | x-java-serialization;charset=UTF-8 |
| FastJsonHttpMessageConverter | Object | */* |
这里只列出重要的两个属性,详细的能够去看org.springframework.http.converter包中的代码。
另外,基本类型都视为对应的包装类的类型来算。还有,基本类型的json序列化就只有字面值,没有key,不属于规范的json序列化,可是基本上全部json框架都支持基本类型直接序列化。
好了,开始说converter的选择逻辑。主要的代码在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor这个类以及它的父类中,这里根据我我的的理解简明地说一下。
先说下写操做的流程,也就是Http序列化。基本都集中在 writeWithMessageConverters 这个方法中。咱们先以Accept = default(*/*)请求 http://localhost:8080/users/1 为例。
第一步是取出请求的MediaType以及咱们可以返回的MediaType,相关代码以下:
HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request); List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); if (outputValue != null && producibleMediaTypes.isEmpty()) { throw new IllegalArgumentException("No converter found for return value of type: " + valueType); }
getAcceptableMediaTypes
: 大体思路就是从Accept中获取MediaType,这里为 [*/*] 。
getProducibleMediaTypes
: 大体思路就是根据是否支持controller方法的返回类型JavaType(这里是User类,实现了Serializable),一个个遍历检查配置上的全部Converter,查看他们是否支持这种Java类型的转换,这里返回值为这里为 [application/json, application/*+json, application/json, application/x-java-serialization;charset=UTF-8, */*]。
按照Java类型规则这里应该有Jaxb2RootElementHttpMessageConverter,可是查看其源码就知道它还须要知足@XmlRootElement注解这个条件才行,因此这里只有上面四个MediaType,对应的三个Converter分别是Jackson、自定义的、FastJson.
第二步,把Accpet指定的MediaType具体化,意思就是req能够指定 * 这种通配符,可是服务端不该该返回带 * 的Content-Type,代码以下:
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType requestedType : requestedMediaTypes) { for (MediaType producibleType : producibleMediaTypes) { if (requestedType.isCompatibleWith(producibleType)) { compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (compatibleMediaTypes.isEmpty()) { if (outputValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; } List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes); MediaType.sortBySpecificityAndQuality(mediaTypes); MediaType selectedMediaType = null; for (MediaType mediaType : mediaTypes) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } }
第一个for循环,寻找出更具体的MediaType,isCompatibleWith方法判断是否兼容,getMostSpecificMediaType方法取两者中更具体的那个。具体的判断逻辑在MediaType类中,这里就不细说了,能够把更具体理解为找出 requestedMediaType 有 instanceof 关系的 producibleMediaType。由于 */* 相似于 Object 因此这里一个都筛不掉,compatibleMediaTypes最后仍是那四个MediaType。
后两行代码是排序,按照 q值 和 具体程度 来排序。由于咱们没有指定q值,因此都是q=1。根据具体程度排序,带 * 的会排到后面。注意那个LinkedHashSet,先来的会排在前面,加上前面的都是list迭代,因此最后的顺序为[application/json, application/x-java-serialization;charset=UTF-8, application/*+json, */*]。
第二个是默认值处理,application/json 是一个具体的类型,不用再处理,因此最后的produce = application/json。
第三步,选择一个能处理最后的produce的Converter,Jackson和FastJson都能处理,根据添加顺序,此时选择的是Jackson,也就是Jackson的优先级更高。
if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { if (((GenericHttpMessageConverter) messageConverter).canWrite( declaredType, valueType, selectedMediaType)) { outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); ((GenericHttpMessageConverter) messageConverter).write( outputValue, declaredType, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } } else if (messageConverter.canWrite(valueType, selectedMediaType)) { outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; } } }
上面的流程解释了为何经过在converters末尾添加FastJsonConverter时,Fiddler的默认请求(不带Accept或者Accept: */*),使用的是Jackson序列化,序列化了createTime字段,而且返回的 Content-Type 为application/json。
可是使用浏览器直接请求时,Chrome的默认请求的Accept为text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8.
根据上面的逻辑,最后的produce = text/html.
最后选择时,只有FastJson(MediaType对应的 */*)这惟一一个converter可以进行处理,因此用的是FastJson序列化,没有序列化createTime字段。返回的 Content-Type 为 text/html,可是实际格式是json的。
而当使用 converters.add(0, fastJsonConverter) (或者其余等价方式)进行配置时,会把FastJsonConverter添加在最前面,优先级最高。由于FastJsonConverter的MediaType是 */*,因此它会在前面包揽全部请求的Http序列化和反序列化,就算它们不是json,也说了本身不是json、不要返回json,它仍是独断独行地当成json处理。
此时不论Accept是什么类型,返回的实际上都是FastJson序列化的json格式,可是返回的Content-Type却仍是别人
Accept 的那种类型,不必定是application/json这类json标识(挂羊头卖狗肉)。
下面再说下读取的流程,也就是反序列化流程,主流程在父类的 readWithMessageConverters 方法中,代码以下:
@SuppressWarnings("unchecked") protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType; boolean noContentType = false; try { contentType = inputMessage.getHeaders().getContentType(); } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } if (contentType == null) { noContentType = true; contentType = MediaType.APPLICATION_OCTET_STREAM; } Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null); Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null); if (targetClass == null) { ResolvableType resolvableType = (parameter != null ? ResolvableType.forMethodParameter(parameter) : ResolvableType.forType(targetType)); targetClass = (Class<T>) resolvableType.resolve(); } HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod(); Object body = NO_VALUE; try { inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage); for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); if (converter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; if (genericConverter.canRead(targetType, contextClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } if (inputMessage.getBody() != null) { inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); body = genericConverter.read(targetType, contextClass, inputMessage); body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); } break; } } else if (targetClass != null) { if (converter.canRead(targetClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); } if (inputMessage.getBody() != null) { inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType); body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType); } break; } } } } catch (IOException ex) { throw new HttpMessageNotReadableException("I/O error while reading input message", ex); } if (body == NO_VALUE) { if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || (noContentType && inputMessage.getBody() == null)) { return null; } throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); } return body; }
由于读取时的ContentType一定是一个具体类型(带有 * 号会抛出异常 java.lang.IllegalArgumentException: 'Content-Type' cannot contain wildcard subtype '*'),因此步骤少了一些,匹配前就一个默认值处理。
默认的请求不带Content-Type,会进行默认值处理,最后 contentType = application/octet-stream,只有FastJsonConverter(MediaType对应的 */*)这惟一一个converter可以进行处理,因此就没有反序列化createTime字段,打印信息user.toString()中createTime=null。
可是当指定Content-Type: application/json时,contentType = application/json,Jackson和FastJson都能处理,按照顺序,轮到Jackson反序列化,因此就反序列化了createTime字段,打印信息user.toString()中createTime不为null。
改为使用 converters.add(0, fastJsonConverter) (或者其余等价方式)进行配置时,会把FastJsonConverter添加在最前面,顺序优先级比Jackson高,指定Content-Type: application/json时使用的就是FastJson来进行反序列化。
可是跟上面说的那样,由于 */* 的缘由,此时不论Content-Type是什么类型,都会是FastJsonConverter来进行反序列化操做。不过,FastJson只是个json框架,只能处理json,别的格式会抛出异常,而且还返回 HTTP 400 告诉客户端你的请求报文格式不对(没有金刚钻,非要揽瓷器活,明明是本身的错,还要说是客户端的错)。
好了,到此就基本上说完了整个HttpMessageConverter的匹配规则(或者叫选择过程)。此次没有新增代码,也没有演示,想要本身演示观察的,能够在上一篇文章相关的代码基础上进行,以下:
https://gitee.com/page12/study-springboot/tree/springboot-3
https://github.com/page12/study-springboot/tree/springboot-3
最后再次吐槽下FastJsonHttpMessageConverter,做为非springmvc自带的组件,默认设置 */* 这种MediaType,是很是很差的。上面也说了,存在挂羊头卖狗肉、名实不副的行为,在REST已经从新引发人们对HTTP原生规范的重视的今天,这是一个很很差的作法。本身能力不是最大,却大包大揽承担最大责任,处理不了还返回 HTTP 400,是甩锅客户端的行为。阿里做为国内第一大开源阵营,其代码设计、质量,以及开源奉献精神仍是要进一步提高啊。
本身写代码也要注意啊:代码中有顺序遍历匹配这种逻辑,或者叫责任链模式时,功能越具体的节点越是应该放在前面,功能最广最泛的节点应该放在最后面;同时要按功能分配责任,千万不要给功能单一的节点最大的责任(FastJsonConverter的功能单一,却分配了个最大的责任 MediaType = */*)。