SpringBoot的HttpMessageConverter使用(1)RestTemplate中的應(yīng)用

場景:某次上線后,導致別的接口RestTemplate調(diào)用出現(xiàn)了異常迫皱。

1. 起因

經(jīng)過排查后發(fā)現(xiàn),某次上線的需求增加了該依賴:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.11.0</version>
    <scope>compile</scope>
</dependency>

被影響的接口:

@Slf4j
@RestController
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    //測試http調(diào)用
    @RequestMapping(value = "/test3")
    public OrderDto get3() {
        String url = "http://localhost:8011/consumer/dept/get";
        OrderDto orderDto = new OrderDto();
        orderDto.setId(100110L);
        orderDto.setName("tom is mao");
        ResponseEntity<String> results = restTemplate.postForEntity(url, orderDto, String.class);
        log.info("打印響應(yīng)報文{}...", results);
        return orderDto;
    }
}

全局RestTemplate配置:

@Configuration
@Slf4j
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
        try {
            HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
            RegistryBuilder<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().
                    register("http", PlainConnectionSocketFactory.getSocketFactory());// 注冊http和https請求
            // 開始設(shè)置連接池
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry.build());
            poolingHttpClientConnectionManager.setMaxTotal(500); // 最大連接數(shù)500
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并發(fā)數(shù)100
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 重試次數(shù)
            HttpClient httpClient = httpClientBuilder.build();
            HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient連接配置
            clientHttpRequestFactory.setConnectTimeout(20000);              // 連接超時
            clientHttpRequestFactory.setReadTimeout(30000);                 // 數(shù)據(jù)讀取超時時間
            clientHttpRequestFactory.setConnectionRequestTimeout(20000);    // 連接不夠用的等待時間
            return clientHttpRequestFactory;
        } catch (Exception e) {
            log.error("初始化HTTP連接池出錯", e);
        }
        return null;
    }
}

經(jīng)過排查后發(fā)現(xiàn):發(fā)送的請求報文被序列化成了xml格式箩做,但該接口之前為JSON格式近忙。

2. 解決方案

2.1 方案一

  1. 顯式的聲明Content-Type的類型:
@Slf4j
@RestController
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    //測試http調(diào)用
    @RequestMapping(value = "/test5")
    public OrderDto get5() {
        String url = "http://localhost:8011/consumer/dept/get";
        
        OrderDto orderDto = new OrderDto();
        orderDto.setId(100110L);
        orderDto.setName("tom is mao");

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<OrderDto> httpEntity = new HttpEntity<>(orderDto,headers);
        
        ResponseEntity<String> results = restTemplate.postForEntity(url, httpEntity, String.class);
        log.info("打印響應(yīng)報文{}...", results);
        return orderDto;
    }
}

2.2 方案二

本次被影響的接口由于沒有顯式的聲明請求的Content-type類型(即使用RestTemplate默認的消息轉(zhuǎn)換器)。而由于引入了jackson-dataformat-xml依賴嘱兼,導致消息轉(zhuǎn)換器由JSON格式轉(zhuǎn)換為XML格式国葬。

2.2.1 消息轉(zhuǎn)換器參與序列化

源碼分析:org.springframework.web.client.RestTemplate.HttpEntityRequestCallback#doWithRequest

@Override
@SuppressWarnings("unchecked")
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
   super.doWithRequest(httpRequest);
   Object requestBody = this.requestEntity.getBody();
   if (requestBody == null) {
      HttpHeaders httpHeaders = httpRequest.getHeaders();
      HttpHeaders requestHeaders = this.requestEntity.getHeaders();
      if (!requestHeaders.isEmpty()) {
         requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
      }
      if (httpHeaders.getContentLength() < 0) {
         httpHeaders.setContentLength(0L);
      }
   }
   else {
      Class<?> requestBodyClass = requestBody.getClass();
      Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
            ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
      HttpHeaders httpHeaders = httpRequest.getHeaders();
      HttpHeaders requestHeaders = this.requestEntity.getHeaders();
      MediaType requestContentType = requestHeaders.getContentType();
      //若requestBody不為空,遍歷消息轉(zhuǎn)換器芹壕,獲取到適合的消息轉(zhuǎn)換
      for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
         if (messageConverter instanceof GenericHttpMessageConverter) {
            GenericHttpMessageConverter<Object> genericConverter =
                  (GenericHttpMessageConverter<Object>) messageConverter;
            //判斷是否可以使用該消息轉(zhuǎn)換器去序列化汇四。
            if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
               if (!requestHeaders.isEmpty()) {
                  requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
               }
               logBody(requestBody, requestContentType, genericConverter);
               genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
               //填充完Request對象后,結(jié)束方法踢涌。
               return;
            }
         }
         else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
            if (!requestHeaders.isEmpty()) {
               requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
            }
            logBody(requestBody, requestContentType, messageConverter);
            ((HttpMessageConverter<Object>) messageConverter).write(
                  requestBody, requestContentType, httpRequest);
            return;
         }
      }
      String message = "No HttpMessageConverter for " + requestBodyClass.getName();
      if (requestContentType != null) {
         message += " and content type \"" + requestContentType + "\"";
      }
      throw new RestClientException(message);
   }
}

由此可以說明通孽,優(yōu)先級高的消息轉(zhuǎn)換器將會生效。

2.2.2 消息轉(zhuǎn)換器被初始化

public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {

    static {
        ClassLoader classLoader = RestTemplate.class.getClassLoader();
        romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
        jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
        jackson2Present =
                ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
                        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
        jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
        jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
        jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
        gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
        jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
    }

    public RestTemplate() {
        this.messageConverters.add(new ByteArrayHttpMessageConverter());
        this.messageConverters.add(new StringHttpMessageConverter());
        this.messageConverters.add(new ResourceHttpMessageConverter(false));
        try {
            this.messageConverters.add(new SourceHttpMessageConverter<>());
        }
        catch (Error err) {
            // Ignore when no TransformerFactory implementation is available
        }
        this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (romePresent) {
            this.messageConverters.add(new AtomFeedHttpMessageConverter());
            this.messageConverters.add(new RssChannelHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
        }
        else if (jaxb2Present) {
            this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            this.messageConverters.add(new MappingJackson2HttpMessageConverter());
        }
        else if (gsonPresent) {
            this.messageConverters.add(new GsonHttpMessageConverter());
        }
        else if (jsonbPresent) {
            this.messageConverters.add(new JsonbHttpMessageConverter());
        }

        if (jackson2SmilePresent) {
            this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
        }
        if (jackson2CborPresent) {
            this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
        }

        this.uriTemplateHandler = initUriTemplateHandler();
    }
}

在調(diào)用RestTemplate的構(gòu)造方法時睁壁,將消息轉(zhuǎn)換器放入到集合中背苦。而決定是否加入messageConverters在靜態(tài)代碼塊(是否引入了某些依賴)。

由于某次需求引入了jackson-dataformat-xml依賴潘明,導致jackson2XmlPresent返回true行剂,由此:

image.png

xml優(yōu)先級比json高。

解決方案:全局修改轉(zhuǎn)換器優(yōu)先級钳降。

    @Bean
    public RestTemplate restTemplate() {

        RestTemplate restTemplate = new RestTemplate();
        //獲取消息轉(zhuǎn)換器
        List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
        messageConverters.add(5,new MappingJackson2HttpMessageConverter());
        return restTemplate;
    }

3. 源碼注意事項

當選中messageConverter后厚宰,其org.springframework.http.converter.GenericHttpMessageConverter#canWrite方法決定是否使用該消息處理器。

以Jackson為例:org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canWrite

    @Override
    public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
        //判斷content-type是否合適牲阁,不合適直接返回false
        if (!canWrite(mediaType)) {
            return false;
        }
        //判斷是否可以使用objectMapper進行序列化固阁,若可以才會返回true、
        AtomicReference<Throwable> causeRef = new AtomicReference<>();
        if (this.objectMapper.canSerialize(clazz, causeRef)) {
            return true;
        }
        logWarningIfNecessary(clazz, causeRef.get());
        return false;
    }
    protected boolean canWrite(@Nullable MediaType mediaType) {
          //若mediaType為空城菊,或者all那么支持
        if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
            return true;
        }
        //判斷子類是否支持該content-type
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.isCompatibleWith(mediaType)) {
                return true;
            }
        }
        return false;
    }

因為我們沒有顯式的聲明content-type备燃,那么MappingJackson2XmlHttpMessageConverter也可以支持序列化的方式。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凌唬,一起剝皮案震驚了整個濱河市并齐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖况褪,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撕贞,死亡現(xiàn)場離奇詭異,居然都是意外死亡测垛,警方通過查閱死者的電腦和手機捏膨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來食侮,“玉大人号涯,你說我怎么就攤上這事【馄撸” “怎么了链快?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長眉尸。 經(jīng)常有香客問我域蜗,道長,這世上最難降的妖魔是什么噪猾? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任霉祸,我火速辦了婚禮,結(jié)果婚禮上畏妖,老公的妹妹穿的比我還像新娘脉执。我一直安慰自己疼阔,他們只是感情好戒劫,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婆廊,像睡著了一般迅细。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淘邻,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天茵典,我揣著相機與錄音,去河邊找鬼宾舅。 笑死统阿,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的筹我。 我是一名探鬼主播扶平,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔬蕊!你這毒婦竟也來了结澄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎麻献,沒想到半個月后们妥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡勉吻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年监婶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片齿桃。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡压储,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出源譬,到底是詐尸還是另有隱情集惋,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布踩娘,位于F島的核電站刮刑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏养渴。R本人自食惡果不足惜雷绢,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望理卑。 院中可真熱鬧翘紊,春花似錦、人聲如沸藐唠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宇立。三九已至踪宠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妈嘹,已是汗流浹背柳琢。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留润脸,地道東北人柬脸。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像毙驯,于是被迫代替她去往敵國和親倒堕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容