SpringBoot的HttpMessageConverter使用(1)RestTemplate中的应用

场景:某次上线后,导致别的接口RestTemplate调用出现了异常。

1. 起因

经过排查后发现,某次上线的需求增加了该依赖:


    com.fasterxml.jackson.dataformat
    jackson-dataformat-xml
    2.11.0
    compile

被影响的接口:

@Slf4j
@RestController
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    //测试http调用
    @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 results = restTemplate.postForEntity(url, orderDto, String.class);
        log.info("打印响应报文{}...", 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 socketFactoryRegistry = RegistryBuilder.create().
                    register("http", PlainConnectionSocketFactory.getSocketFactory());// 注册http和https请求
            // 开始设置连接池
            PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry.build());
            poolingHttpClientConnectionManager.setMaxTotal(500); // 最大连接数500
            poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并发数100
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 重试次数
            HttpClient httpClient = httpClientBuilder.build();
            HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient连接配置
            clientHttpRequestFactory.setConnectTimeout(20000);              // 连接超时
            clientHttpRequestFactory.setReadTimeout(30000);                 // 数据读取超时时间
            clientHttpRequestFactory.setConnectionRequestTimeout(20000);    // 连接不够用的等待时间
            return clientHttpRequestFactory;
        } catch (Exception e) {
            log.error("初始化HTTP连接池出错", e);
        }
        return null;
    }
}

经过排查后发现:发送的请求报文被序列化成了xml格式,但该接口之前为JSON格式。

2. 解决方案

2.1 方案一

  1. 显式的声明Content-Type的类型:
@Slf4j
@RestController
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    //测试http调用
    @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 httpEntity = new HttpEntity<>(orderDto,headers);
        
        ResponseEntity results = restTemplate.postForEntity(url, httpEntity, String.class);
        log.info("打印响应报文{}...", results);
        return orderDto;
    }
}

2.2 方案二

本次被影响的接口由于没有显式的声明请求的Content-type类型(即使用RestTemplate默认的消息转换器)。而由于引入了jackson-dataformat-xml依赖,导致消息转换器由JSON格式转换为XML格式。

2.2.1 消息转换器参与序列化

源码分析: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不为空,遍历消息转换器,获取到适合的消息转换
      for (HttpMessageConverter messageConverter : getMessageConverters()) {
         if (messageConverter instanceof GenericHttpMessageConverter) {
            GenericHttpMessageConverter genericConverter =
                  (GenericHttpMessageConverter) messageConverter;
            //判断是否可以使用该消息转换器去序列化。
            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对象后,结束方法。
               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) 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);
   }
}
 
 

由此可以说明,优先级高的消息转换器将会生效。

2.2.2 消息转换器被初始化

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();
    }
}

在调用RestTemplate的构造方法时,将消息转换器放入到集合中。而决定是否加入messageConverters在静态代码块(是否引入了某些依赖)。

由于某次需求引入了jackson-dataformat-xml依赖,导致jackson2XmlPresent返回true,由此:

image.png

xml优先级比json高。

解决方案:全局修改转换器优先级。

    @Bean
    public RestTemplate restTemplate() {

        RestTemplate restTemplate = new RestTemplate();
        //获取消息转换器
        List> 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 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也可以支持序列化的方式。

你可能感兴趣的:(SpringBoot的HttpMessageConverter使用(1)RestTemplate中的应用)