SpringMVC HttpMessageConverter 匹配規則

如下內容,若有問題,煩請指出,謝謝!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 Acceptablespring

二、若是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 = */*)。

相關文章
相關標籤/搜索