SpringMVC HttpMessageConverter 匹配规则

以下内容,如有问题,烦请指出,谢谢!



SpringMVC启动时会自动配置一些HttpMessageConverter,接收到http请求时,从这些Converters中选择一个符合条件的来进行Http序列化/反序列化。在不覆盖默认的HttpMessageConverters的情况下,我们添加的Converter可能会与默认的产生冲突,在某些场景中出现不符合预期的情况。

上一篇文章的末尾已经列举了一个jsonConverter冲突的情况:添加一个最低优先级的FastJsonConverter后会有两个(实际上三个,有两个jackson的)jsonConverter,直接使用浏览器访问接口时使用的却是低优先级的FastJsonConverter来进行序列化操作。

为了解决converters之间的冲突,或者直接叫优先级问题,需要弄懂SpringMVC是如何选择一个HttpMessageMessagerConverter来进行Http序列化/反序列化的。这篇文章主要就根据相关的代码来讲解SpringMVC的这个内部流程,这块的逻辑比较清晰,贴贴代码就基本上都明白了。



首先需要了解一些HTTP的基本知识(不是强制的而是一种建议与约定):

1、决定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

2、如果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.

前面几个都不支持时,服务器可以自行处理 */*,返回一种服务器自己支持的格式。

3、一个HTTP请求没有指定Accept,默认视为指定 Accept: */*;没有指定Content-Type,默认视为 null,就是没有。当然,服务端可以根据自己的需要改变默认值。

4、Content-Type必须是具体确定的类型,不能包含 *.

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 = */*)。

原文地址:https://www.cnblogs.com/page12/p/8168107.html

时间: 2024-10-09 18:05:31

SpringMVC HttpMessageConverter 匹配规则的相关文章

SpringMVC路径匹配规则AntPathMatcher(转)

SpringMVC的路径匹配规则是依照Ant的来的. 实际上不只是SpringMVC,整个Spring框架的路径解析都是按照Ant的风格来的. 在Spring中的具体实现,详情参见 org.springframework.util.AntPathMatcher. 具体规则如下(来自Spring AntPathMatcher源码注释): * {@link PathMatcher} implementation for Ant-style path patterns. * * <p>Part of

SpringMVC路径匹配规则AntPathMatcher

? 匹配1个字符 * 匹配0个或多个字符 ** 匹配路径中的0个或多个目录 {spring:[a-z]+} 将正则表达式[a-z]+匹配到的值,赋值给名为 spring 的路径变量.(PS:必须是完全匹配才行,在SpringMVC中只有完全匹配才会进入controller层的方法)

SpringMVC路径匹配规则源码

package cc.zeelan.framework.interceptor.permission; /* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. *

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则

Caliburn.Micro学习笔记(一)----引导类和命名匹配规则 用了几天时间看了一下开源框架Caliburn.Micro 这是他源码的地址http://caliburnmicro.codeplex.com/ 文档也写的很详细,自己在看它的文档和代码时写了一些demo和笔记,还有它实现的原理记录一下 学习Caliburn.Micro要有MEF和MVVM的基础 先说一下他的命名规则和引导类 以后我会把Caliburn.Micro的 Actions IResult,IHandle ICondu

在JaveWeb项目中配置Spring 匿名访问时,匹配规则的变相实现/*

实现/* /** * 根据当前的URL返回该url的角色集合. * 1.如果当前的URL在匿名访问的URL集合当中时,在当前的角色中添加匿名访问的角色(SysRole.ROLE_CONFIG_ANONYMOUS). * 2.如果当前系统不存在的情况,给当前用户添加一个公共访问的角色(SysRole.ROLE_CONFIG_PUBLIC). 3.url * 和角色映射,url和参数映射,给当前用户添加一个公共的角色(SysRole.ROLE_CONFIG_PUBLIC). * * @param o

Nginx之location 匹配规则详解

Nginx之location 匹配规则详解 关于一些对location认识的误区 1. location 的匹配顺序是"先匹配正则,再匹配普通". 矫正: location 的匹配顺序其实是"先匹配普通,再匹配正则".我这么说,大家一定会反驳我,因为按"先匹配普通,再匹配正则"解释不了大家平时习惯的按"先匹配正则,再匹配普通"的实践经验.这里我只能暂时解释下,造成这种误解的原因是:正则匹配会覆盖普通匹配(实际的规则,比这复杂,

Android中IntentFilter匹配规则详解——Android开发艺术探索笔记

欢迎转载,转载请注明出处http://blog.csdn.net/l664675249/article/details/50640288 启动Activity的方式分为两种,显示和隐式调用.显示调用很简单,直接指明要启动的Activity就可以了,这里主要介绍一下隐式调用.隐式调用需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息.只有一个Intent同时匹配action,category和data才算匹配成功. 示例 <intent-filter> <acti

JavaScript中正则表达式判断匹配规则以及常用的方法

JavaScript中正则表达式判断匹配规则以及常用的方法: 字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在. 正则表达式是一种用来匹配字符串的强有力的武器.它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它"匹配"了. \d可以匹配一个数字                 '00\d'可以匹配'007' ,'\d\d\d'可以匹配'010' \w可以匹配一个字母或数字      '\w\w'可以匹配'js' \s可

servlet的url-pattern匹配规则详细描述

一.概述 在利用servlet或Filter进行url请求的匹配时,很关键的一点就是匹配规则,但servlet容器中的匹配规则既不是简单的通配,也不是正则表达式,而是由自己的规则,比较容易混淆.本文来详细举例介绍下.下面的说明都是在tomcat服务器中得到验证的. 先介绍一下匹配的概念,上例子代码.在一个app(如名字为myapp)的web.xml文件中,有如下信息: <servlet> <servlet-name>MyServlet</servlet-name> &l