HttpMessageConverter使用不當導致的問題及其原理、配置

兩個問題

  • spring boot RestTemplate在運行一段時間後居然報空指針異常,可以根據StackTrace定位到是有一個HttpMessageConverter爲空;
  • 一個接口返回的Content-typetext/pain,但返回結果總是被雙引號包圍,導致接口調用方解析失敗,可以定位到是由於一個自定義的HttpMessageConverter錯誤的攔截了text/plainproduce類型,並在字符串兩端加上了雙引號。

HttpMessageConverter作用

HttpMessageConverter可以把不同類型的body轉爲Java對象,也可以吧Java對象轉爲滿足要求的body,在序列化與反序列化中有非常重要的作用。

HttpMessageConverter匹配規則

  • Http請求頭中會包含Accept,告知服務器要傳回什麼樣的數據,如Accept: application/json, text/javascript, */*; q=0.01;同時會指定Content-Type告知服務器本次body傳輸的參數數據是什麼類型,服務器可以根據數據類型轉化爲服務器內部對象並提取參數;
  • Server端接收到請求,會判斷自己是否支持客戶端傳過來的參數類型(MediaType),如果沒有任何一個支持,服務器將返回406(HttpMediaTypeNotSupportedException);
  • Server處理完參數和邏輯,準備根據客戶端要求的Accept返回,這時服務端又會判斷自己是否支持返回這種類型(MediaType),如果不支持,服務器將返回406(HttpMediaTypeNotAcceptableException);
  • HttpMessageConverter的作用就是服務器判斷自己是否支持某種MediaType
  • HttpMessageConverter不是僅僅只有一個而是一個列表,通過責任鏈的方式匹配:循序遍歷所有HttpMessageConverter,調用其canRead()方法,若返回true表示可以處理,一旦有某個HttpMessageConverter可以處理某一請求的參數MediaType,就是用這個HttpMessageConverterread()方法讀取參數,一旦處理完數據即將返回,又用同樣的方法遍歷HttpMessageConverter列表,找出第一個canWrite()返回true的HttpMessageConverter,調用其write()方法返回給客戶端。

HttpMessageConverter初始化時序圖

在這裏插入圖片描述

  1. ApplicationContextrefresh期間,HttpMessageConverter開始初始化,初始化分別在三個類中進行:其中RequestMappingHandlerAdapterHttpMessageConvertersAutoConfiguration是同時行的互不干擾,HttpMessageConverters的初始化需要在前兩個類初始化完成後才能進行;
  2. RequestMappingHandlerAdapter初始化默認HttpMessageConverter列表
    • 調用所有WebMvcConfigurer類型的自定義配置類(@Configuration)的configureMessageConverters方法初始化 HttpMessageConverter列表;
    • 如果沒有自定義的WebMvcConfigurer配置,調用addDefaultHttpMessageConverters方法初始化HttpMessageConverter列表,默認HttpMessageConverter列表都是根據ClassLoader中是否加載否一個特定類來判斷某一個HttpMessageConverter是否需要加到默認列表中,並且在最後做了一下排序,僅僅是把xml類型的轉換器放到了目前的HttpMessageConverter列表最後;
    • 調用所有WebMvcConfigurer類型的自定義配置類的extendMessageConverters方法擴展HttpMessageConverter列表,直接加在列表尾部
  3. HttpMessageConvertersAutoConfiguration類將所有HttpMessageConverter類型的組件(@Conponent/@Bean等),初始化到一個有上下文提供的 HttpMessageConverter列表中;
  4. HttpMessageConverters初始化時,將2和3兩個列表合併,如果上下文提供的和默認列表中有重複但對象並非是同一個,會把上下文提供的HttpMessageConverter和默認列表中的HttpMessageConverter放在相鄰的位置,並且會把上下文提供的放在前面;把所有上下文提供的且不在默認列表中的HttpMessageConverter放在整個合併列表的最前面,上下文提供的HttpMessageConverter順序由類上的@Order(value=1)註解指定,value值越小越靠前,優先級越高。

自定義HttpMessageConverter

package com.enmo.dbaas.common.config.feigninterceptor;

import org.springframework.core.annotation.Order;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import javax.activation.UnsupportedDataTypeException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * Created by IntelliJ IDEA
 *
 * @author chenlei
 * @date 2020/1/11
 * @time 17:44
 * @desc AHttpMessageConverter
 */
@Component
@Order(1)
public class AHttpMessageConverter extends AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
    @Override
    protected boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return null;
    }

    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        outputMessage.getBody().write("{}".getBytes());
        outputMessage.getBody().flush();
    }

    @Override
    public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
        return true;
    }

    @Override
    public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        InputStream in = inputMessage.getBody();
        if (String.class.getTypeName().equals(type.getTypeName())) {
            byte[] bytes = new byte[65536];
            int offset = 0;

            while(true) {
                int readCount = in.read(bytes, offset, bytes.length - offset);
                if(readCount == -1) {
                    return new String(bytes, "UTF-8");
                }

                offset += readCount;
                if(offset == bytes.length) {
                    byte[] newBytes = new byte[bytes.length * 3 / 2];
                    System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
                    bytes = newBytes;
                }
            }
        }
        throw new UnsupportedDataTypeException(type.getClass().getTypeName());
    }

    @Override
    public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
        return true;
    }

    @Override
    public void write(Object o, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        super.write(o, contentType, outputMessage);
    }
}

自定義一個AHttpMessageConvertercanRead()canWrite()犯法都直接返回true,並且把@Order值設置爲1,因爲使用Component註解的上下文HttpMessageConverter列表回被添加在HttpMessageConverter前面,而spring默認實現的HttpMessageConverterorder值都是Integer.MAX_VALUE,因此AHttpMessageConverter會匹配所有Content-TypeAccept

類中read()方法只支持String類型的參數,其他類型的參數一律拋出UnsupportedDataTypeException
類中write()方法均返回字符串"{}"

假設在Controller中寫一個這樣的方法:

@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
                         @RequestParam String b,
                         @RequestParam String c,
                         @RequestBody JSONObject d) {
    log.info("{}, {}, {} ,{}", a, b, c, d);
    return new JSONObject().fluentPut("key", "value");
}

將報錯:

org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is javax.activation.UnsupportedDataTypeException: java.lang.Class
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:216)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:88)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:114)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:104)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:745)
Caused by: javax.activation.UnsupportedDataTypeException: java.lang.Class
	at com.enmo.dbaas.common.config.feigninterceptor.AHttpMessageConverter.read(AHttpMessageConverter.java:72)
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)
	... 62 common frames omitted

如果修改JSONObjectString

@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
                         @RequestParam String b,
                         @RequestParam String c,
                         @RequestBody String d) {
    log.info("{}, {}, {} ,{}", a, b, c, d);
    return new JSONObject().fluentPut("key", "value");
}

將返回:

{}

而不是:

{
	"key": "value"
}

說明正是AcceptContent-Type都使用了AHttpMessageConverter來序列化和反序列化。

解決問題

RestTemplate NPE

spring boot RestTemplate在運行一段時間後居然報空指針異常,可以根據StackTrace定位到是有一個HttpMessageConverter爲空

過一段時間才爲空,肯定是運行過程中有地方把HttpMessageConverter列表某個索引的值改爲了null,不是一啓動就報錯,也不是調用特定接口報錯,而是某一次調用出現NPE後,之後的任意一次請求全部報NPE。

恰好找到了這篇文章:處理restTemplate的messageConverters設置StringHttpMessageConverter

文中提到每次調用RestTemplate的時候手動設置字符集爲UTF-8,線上偶發NPE,這與我們的設置極爲相似:

restTemplate.getMessageConverters()
        .add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));

我們知道HttpMessageConverters類中的List<HttpMessageConverter>只是一個簡單的UnmodifiableList<HttpMessageConverter<?>>不支持高併發,這樣要麼會導致List<HttpMessageConverter>無限膨脹,要麼會導致空指針,因爲List.add(int index, Object o)原理是先把列表index之後的所有元素後移一位,然後再把索引爲index的位置賦值。spring的組件默認是單例的,在高併發的情況下,所有線程同時修改一個不支持高併發的列表某,一個線程剛好把所有元素後移一位u,第一個元素還沒來得及賦值,可能恰好另外一個線程已經開始遍歷,取到第一個HttpMessageConverter恰好是null,於是開始報空指針,這是偶發原因;

那爲什麼可能出現一次後,就有可能之後所有的請求全部報空指針呢,特別是在凌晨有多個定時任務同時跑的情況下,出現“永久NPE”的概率極大。這是因爲剛好有兩個線程同時設置索引爲0的HttpMessageConverter,其中一個線程剛剛把所有元素後移一位,還沒來得及給索引爲0的位置賦值,另一個線程又開始把所有元素後移,導致索引爲1的元素永久爲空,再也救不回來了。

找的文章裏的解決方案和單例模式類似,但也並不能完全避免高併發問題,還是需要加同步塊並且使用duoble check,這種方式非常不優雅,因爲spring 的bean默認是單例,我們完全可以在初始化的時候配置好,後面直接注入使用即可,不需要二次配置:

@Configuration
public class HttpConfiguration {
    @Bean
    RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
        messageConverters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
}

text/pain有引號

一個接口返回的Content-typetext/pain,但返回結果總是被雙引號包圍,導致接口調用方解析失敗,可以定位到是由於一個自定義的HttpMessageConverter錯誤的攔截了text/plainproduce類型,並在字符串兩端加上了雙引號。

理解上述原理後,應該可以猜想到應該是由錯誤的HttpMessageConverter序列化導致的,在不配置任何自定義HttpMessageConverter的時候一切正常,我們只加了一個自定義的FastJsonHttpMessageConverter

package com.enmo.dbaas.common.config;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class FastJsonHttpMessageConverterEx extends FastJsonHttpMessageConverter {
    public FastJsonHttpMessageConverterEx() {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");    // 自定義時間格式
        fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue); // 正常轉換 null 值
        List<MediaType> supportedMediaTypes = new ArrayList<>();
        supportedMediaTypes.add(MediaType.APPLICATION_JSON);
        supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
        supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
        supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
        supportedMediaTypes.add(MediaType.APPLICATION_PDF);
        supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
        supportedMediaTypes.add(MediaType.APPLICATION_XML);
        supportedMediaTypes.add(MediaType.IMAGE_GIF);
        supportedMediaTypes.add(MediaType.IMAGE_JPEG);
        supportedMediaTypes.add(MediaType.IMAGE_PNG);
        supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
        supportedMediaTypes.add(MediaType.TEXT_HTML);
        supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
        supportedMediaTypes.add(MediaType.TEXT_XML);
        supportedMediaTypes.add(MediaType.TEXT_PLAIN);
        this.setSupportedMediaTypes(supportedMediaTypes);
        this.setFastJsonConfig(fastJsonConfig);
    }
}

這是上下文提供的,會放在HttpMessageConverter列表的第一個,優先級最高。
結合Fastjson序列化字符串的時候會加上引號:
在這裏插入圖片描述
因此肯定是這個FastJsonHttpMessageConverterEx的鍋,果不其然,FastJsonHttpMessageConvertercanRead()方法和canWrite()方法都是是根據List<MediaType> supportedMediaTypes來判斷的:只要是List<MediaType> supportedMediaTypes定義的MediaType,都由FastJsonHttpMessageConverterEx來序列化和反序列化,而我們自定的FastJsonHttpMessageConverterEx加上了supportedMediaTypes.add(MediaType.TEXT_PLAIN);,不出錯就奇怪了。

刪掉該行,問題解決,並且FastJsonHttpMessageConverterEx該不該處理這麼多類型還需要多考量,雖然目前系統運作良好,但是不保證新業務增加後會出現問題,因爲目前我們只有applicaition/json;plain/text;application/object-stream等幾種類型,假設哪天出現一個image/jpeg,保不齊又會出錯,因爲可能Fastjson處理不了但又強行讓其處理,也可能強行處理了,結果格式不對等。

發佈了78 篇原創文章 · 獲贊 20 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章