Spring Boot使用RestTemplate消费REST服务的几个问题记录

我们可以通过Spring Boot快速开发REST接口,同时也可能需要在实现接口的过程中,通过Spring Boot调用内外部REST接口完成业务逻辑。

在Spring Boot中,调用REST Api常见的一般主要有两种方式,通过自带的RestTemplate或者自己开发http客户端工具实现服务调用。

RestTemplate基本功能非常强大,不过某些特殊场景,我们可能还是更习惯用自己封装的工具类,比如上传文件至分布式文件系统、处理带证书的https请求等。

本文以RestTemplate来举例,记录几个使用RestTemplate调用接口过程中发现的问题和解决方案。

一、RestTemplate简介

1、什么是RestTemplate

我们自己封装的HttpClient,通常都会有一些模板代码,比如建立连接,构造请求头和请求体,然后根据响应,解析响应信息,最后关闭连接。

RestTemplate是Spring中对HttpClient的再次封装,简化了发起HTTP请求以及处理响应的过程,抽象层级更高,减少消费者的模板代码,使冗余代码更少。

其实仔细想想Spring Boot下的很多XXXTemplate类,它们也提供各种模板方法,只不过抽象的层次更高,隐藏了更多细节而已。

顺便提一下,Spring Cloud有一个声明式服务调用Feign,是基于Netflix Feign实现的,整合了Spring Cloud Ribbon与 Spring Cloud Hystrix,并且实现了声明式的Web服务客户端定义方式。

本质上Feign是在RestTemplate的基础上对其再次封装,由它来帮助我们定义和实现依赖服务接口的定义。

2、RestTemplate常见方法

常见的REST服务有很多种请求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate实现了最常见的方式,用的最多的就是Get和Post了,调用API可参考源码,这里列举几个方法定义(GET、POST、DELETE):

methods

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) 

public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)

public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables)

public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)

public void delete(String url, Object... uriVariables)

public void delete(URI url)

同时要注意两个较为“灵活”的方法 exchange 和 execute 。

RestTemplate暴露的exchange与其它接口的不同:

(1)允许调用者指定HTTP请求的方法(GET,POST,DELETE等)

(2)可以在请求中增加body以及头信息,其内容通过参数‘HttpEntity<?>requestEntity'描述

(3)exchange支持‘含参数的类型'(即泛型类)作为返回类型,该特性通过‘ParameterizedTypeReference<T>responseType'描述。

RestTemplate所有的GET,POST等等方法,最终调用的都是execute方法。excute方法的内部实现是将String格式的URI转成了java.net.URI,之后调用了doExecute方法,doExecute方法的实现如下:

doExecute

 /**
  * Execute the given method on the provided URI.
  * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
  * the response with the {@link ResponseExtractor}.
  * @param url the fully-expanded URL to connect to
  * @param method the HTTP method to execute (GET, POST, etc.)
  * @param requestCallback object that prepares the request (can be {@code null})
  * @param responseExtractor object that extracts the return value from the response (can be {@code null})
  * @return an arbitrary object, as returned by the {@link ResponseExtractor}
  */
 @Nullable
 protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
   @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

  Assert.notNull(url, "'url' must not be null");
  Assert.notNull(method, "'method' must not be null");
  ClientHttpResponse response = null;
  try {
   ClientHttpRequest request = createRequest(url, method);
   if (requestCallback != null) {
    requestCallback.doWithRequest(request);
   }
   response = request.execute();
   handleResponse(url, method, response);
   if (responseExtractor != null) {
    return responseExtractor.extractData(response);
   }
   else {
    return null;
   }
  }
  catch (IOException ex) {
   String resource = url.toString();
   String query = url.getRawQuery();
   resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
   throw new ResourceAccessException("I/O error on " + method.name() +
     " request for \"" + resource + "\": " + ex.getMessage(), ex);
  }
  finally {
   if (response != null) {
    response.close();
   }
  }
 }

doExecute方法封装了模板方法,比如创建连接、处理请求和应答,关闭连接等。

多数人看到这里,估计都会觉得封装一个RestClient不过如此吧?

3、简单调用

以一个POST调用为例:

GoodsServiceClient

package com.power.demo.restclient;

import com.power.demo.common.AppConst;
import com.power.demo.restclient.clientrequest.ClientGetGoodsByGoodsIdRequest;
import com.power.demo.restclient.clientresponse.ClientGetGoodsByGoodsIdResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

/**
 * 商品REST接口客户端 (demo测试用)
 **/
@Component
public class GoodsServiceClient {

 //服务消费者调用的接口URL 形如:http://localhost:9090
 @Value("${spring.power.serviceurl}")
 private String _serviceUrl;

 @Autowired
 private RestTemplate restTemplate;

 public ClientGetGoodsByGoodsIdResponse getGoodsByGoodsId(ClientGetGoodsByGoodsIdRequest request) {
  String svcUrl = getGoodsSvcUrl() + "/getinfobyid";

  ClientGetGoodsByGoodsIdResponse response = null;

  try {
   response = restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.class);
  } catch (Exception e) {
   e.printStackTrace();
   response = new ClientGetGoodsByGoodsIdResponse();
   response.setCode(AppConst.FAIL);
   response.setMessage(e.toString());
  }

  return response;
 }

 private String getGoodsSvcUrl() {

  String url = "";

  if (_serviceUrl == null) {
   _serviceUrl = "";
  }
  if (_serviceUrl.length() == 0) {
   return url;
  }

  if (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/") {
   url = String.format("%sapi/v1/goods", _serviceUrl);
  } else {
   url = String.format("%s/api/v1/goods", _serviceUrl);
  }

  return url;
 }
}

demo里直接RestTemplate.postForObject方法调用,反序列化实体转换这些RestTemplate内部封装搞定。

二、问题汇总

1、no suitable HttpMessageConverter found for request type异常

这个问题通常会出现在postForObject中传入对象进行调用的时候。

分析RestTemplate源码,在HttpEntityRequestCallback类的doWithRequest方法中,如果 messageConverters (这个字段后面会继续提及)列表字段循环处理的过程中没有满足return跳出的逻辑(也就是没有匹配的HttpMessageConverter),则抛出上述异常:

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()) {
     for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
      httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
     }
    }
    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();
    for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
     if (messageConverter instanceof GenericHttpMessageConverter) {
      GenericHttpMessageConverter<Object> genericConverter =
        (GenericHttpMessageConverter<Object>) messageConverter;
      if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
       if (!requestHeaders.isEmpty()) {
        for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
         httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
        }
       }
       if (logger.isDebugEnabled()) {
        if (requestContentType != null) {
         logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
           "\" using [" + messageConverter + "]");
        }
        else {
         logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
        }

       }
       genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
       return;
      }
     }
     else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
      if (!requestHeaders.isEmpty()) {
       for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
        httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
       }
      }
      if (logger.isDebugEnabled()) {
       if (requestContentType != null) {
        logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
          "\" using [" + messageConverter + "]");
       }
       else {
        logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
       }

      }
      ((HttpMessageConverter<Object>) messageConverter).write(
        requestBody, requestContentType, httpRequest);
      return;
     }
    }
    String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
      requestBodyClass.getName() + "]";
    if (requestContentType != null) {
     message += " and content type [" + requestContentType + "]";
    }
    throw new RestClientException(message);
   }
  }

最简单的解决方案是,可以通过包装http请求头,并将请求对象序列化成字符串的形式传参,参考示例代码如下:

postForObject

 /*
  * Post请求调用
  * */
 public static String postForObject(RestTemplate restTemplate, String url, Object params) {
  HttpHeaders headers = new HttpHeaders();
  MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
  headers.setContentType(type);
  headers.add("Accept", MediaType.APPLICATION_JSON.toString());

  String json = SerializeUtil.Serialize(params);

  HttpEntity<String> formEntity = new HttpEntity<String>(json, headers);

  String result = restTemplate.postForObject(url, formEntity, String.class);

  return result;
 }

如果我们还想直接返回对象,直接反序列化返回的字符串即可:

postForObject

 /*
  * Post请求调用
  * */
 public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) {
  T response = null;

  String respStr = postForObject(restTemplate, url, params);

  response = SerializeUtil.DeSerialize(respStr, clazz);

  return response;
 }

其中,序列化和反序列化工具比较多,常用的比如fastjson、jackson和gson。

2、no suitable HttpMessageConverter found for response type异常

和发起请求发生异常一样,处理应答的时候也会有问题。

StackOverflow上有人问过相同的问题,根本原因是HTTP消息转换器HttpMessageConverter缺少 MIME Type ,也就是说HTTP在把输出结果传送到客户端的时候,客户端必须启动适当的应用程序来处理这个输出文档,这可以通过多种MIME(多功能网际邮件扩充协议)Type来完成。

对于服务端应答,很多HttpMessageConverter默认支持的媒体类型(MIMEType)都不同。StringHttpMessageConverter默认支持的则是MediaType.TEXT_PLAIN,SourceHttpMessageConverter默认支持的则是MediaType.TEXT_XML,FormHttpMessageConverter默认支持的是MediaType.APPLICATION_FORM_URLENCODED和MediaType.MULTIPART_FORM_DATA,在REST服务中,我们用到的最多的还是 MappingJackson2HttpMessageConverter ,这是一个比较通用的转化器(继承自GenericHttpMessageConverter接口),根据分析,它默认支持的MIMEType为MediaType.APPLICATION_JSON:

MappingJackson2HttpMessageConverter

 /**
  * Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
  * You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
  * @see Jackson2ObjectMapperBuilder#json()
  */
 public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
  super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
 }

但是有些应用接口默认的应答MIMEType不是application/json,比如我们调用一个外部天气预报接口,如果使用RestTemplate的默认配置,直接返回一个字符串应答是没有问题的:

String url = "http://wthrcdn.etouch.cn/weather_mini?city=上海";
String result = restTemplate.getForObject(url, String.class);
ClientWeatherResultVO vo = SerializeUtil.DeSerialize(result, ClientWeatherResultVO.class);

但是,如果我们想直接返回一个实体对象:

String url = "http://wthrcdn.etouch.cn/weather_mini?city=上海";

ClientWeatherResultVO weatherResultVO = restTemplate.getForObject(url, ClientWeatherResultVO.class);

则直接报异常:

Could not extract response: no suitable HttpMessageConverter found for response type [class ]

and content type [application/octet-stream]

很多人碰到过这个问题,首次碰到估计大多都比较懵吧,很多接口都是json或者xml或者plain text格式返回的,什么是application/octet-stream?

查看RestTemplate源代码,一路跟踪下去会发现 HttpMessageConverterExtractor 类的extractData方法有个解析应答及反序列化逻辑,如果不成功,抛出的异常信息和上述一致:

HttpMessageConverterExtractor.extractData

 @Override
 @SuppressWarnings({"unchecked", "rawtypes", "resource"})
 public T extractData(ClientHttpResponse response) throws IOException {
  MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
  if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
   return null;
  }
  MediaType contentType = getContentType(responseWrapper);

  try {
   for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
    if (messageConverter instanceof GenericHttpMessageConverter) {
     GenericHttpMessageConverter<?> genericMessageConverter =
       (GenericHttpMessageConverter<?>) messageConverter;
     if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
      if (logger.isDebugEnabled()) {
       logger.debug("Reading [" + this.responseType + "] as \"" +
         contentType + "\" using [" + messageConverter + "]");
      }
      return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
     }
    }
    if (this.responseClass != null) {
     if (messageConverter.canRead(this.responseClass, contentType)) {
      if (logger.isDebugEnabled()) {
       logger.debug("Reading [" + this.responseClass.getName() + "] as \"" +
         contentType + "\" using [" + messageConverter + "]");
      }
      return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
     }
    }
   }
  }
  catch (IOException | HttpMessageNotReadableException ex) {
   throw new RestClientException("Error while extracting response for type [" +
     this.responseType + "] and content type [" + contentType + "]", ex);
  }

  throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " +
    "for response type [" + this.responseType + "] and content type [" + contentType + "]");
 }

StackOverflow上的解决的示例代码可以接受,但是并不准确,常见的MIMEType都应该加进去,贴一下我认为正确的代码:

RestTemplateConfig

package com.power.demo.restclient.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.*;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;

@Component
public class RestTemplateConfig {

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

 // 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
 @Autowired
 private RestTemplateBuilder builder;

 @Autowired
 private ObjectMapper objectMapper;

 // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
 @Bean
 public RestTemplate restTemplate() {

  RestTemplate restTemplate = builder.build();

  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  converter.setObjectMapper(objectMapper);

  //不加会出现异常
  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]

  MediaType[] mediaTypes = new MediaType[]{
    MediaType.APPLICATION_JSON,
    MediaType.APPLICATION_OCTET_STREAM,

    MediaType.APPLICATION_JSON_UTF8,
    MediaType.TEXT_HTML,
    MediaType.TEXT_PLAIN,
    MediaType.TEXT_XML,
    MediaType.APPLICATION_STREAM_JSON,
    MediaType.APPLICATION_ATOM_XML,
    MediaType.APPLICATION_FORM_URLENCODED,
    MediaType.APPLICATION_PDF,
  };

  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

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

  messageConverters.add(new FormHttpMessageConverter());

  messageConverters.add(new ByteArrayHttpMessageConverter());
  messageConverters.add(new StringHttpMessageConverter());
  messageConverters.add(new ResourceHttpMessageConverter(false));
  messageConverters.add(new SourceHttpMessageConverter());
  messageConverters.add(new AllEncompassingFormHttpMessageConverter());
  if (romePresent) {
   messageConverters.add(new AtomFeedHttpMessageConverter());
   messageConverters.add(new RssChannelHttpMessageConverter());
  }

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

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

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

  restTemplate.setMessageConverters(messageConverters);

  return restTemplate;
 }

}

看到上面的代码,再对比一下RestTemplate内部实现,就知道我参考了RestTemplate的源码,有洁癖的人可能会说这一坨代码有点啰嗦,上面那一堆static final的变量和messageConverters填充数据方法,暴露了RestTemplate的实现,如果RestTemplate修改了,这里也要改,非常不友好,而且看上去一点也不OO。

经过分析,RestTemplateBuilder.build()构造了RestTemplate对象,只要将内部MappingJackson2HttpMessageConverter修改一下支持的MediaType即可,RestTemplate的messageConverters字段虽然是private final的,我们依然可以通过反射修改之,改进后的代码如下:

RestTemplateConfig

package com.power.demo.restclient.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Component
public class RestTemplateConfig {

 // 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
 @Autowired
 private RestTemplateBuilder builder;

 @Autowired
 private ObjectMapper objectMapper;

 // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
 @Bean
 public RestTemplate restTemplate() {

  RestTemplate restTemplate = builder.build();

  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  converter.setObjectMapper(objectMapper);

  //不加可能会出现异常
  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]

  MediaType[] mediaTypes = new MediaType[]{
    MediaType.APPLICATION_JSON,
    MediaType.APPLICATION_OCTET_STREAM,

    MediaType.TEXT_HTML,
    MediaType.TEXT_PLAIN,
    MediaType.TEXT_XML,
    MediaType.APPLICATION_STREAM_JSON,
    MediaType.APPLICATION_ATOM_XML,
    MediaType.APPLICATION_FORM_URLENCODED,
    MediaType.APPLICATION_JSON_UTF8,
    MediaType.APPLICATION_PDF,
  };

  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

  try {
   //通过反射设置MessageConverters
   Field field = restTemplate.getClass().getDeclaredField("messageConverters");

   field.setAccessible(true);

   List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate);

   Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream()
     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
       .getName()))
     .findFirst();

   if (opConverter.isPresent() == false) {
    return restTemplate;
   }

   messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter

   //添加原有的剩余的HttpMessageConverter
   List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream()
     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
       .getName()) == false)
     .collect(Collectors.toList());

   messageConverters.addAll(leftConverters);

   System.out.println(String.format("【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"
     , orgConverterList.size(), messageConverters.size()));

  } catch (Exception e) {
   e.printStackTrace();
  }

  restTemplate.setMessageConverters(messageConverters);

  return restTemplate;
 }
}

除了一个messageConverters字段,看上去我们不再关心RestTemplate那些外部依赖包和内部构造过程,果然干净简洁好维护了很多。

3、乱码问题

这个也是一个非常经典的问题。解决方案非常简单,找到HttpMessageConverter,看看默认支持的Charset。AbstractJackson2HttpMessageConverter是很多HttpMessageConverter的基类,默认编码为UTF-8:

AbstractJackson2HttpMessageConverter

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {

 public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

}

而StringHttpMessageConverter比较特殊,有人反馈过发生乱码问题由它默认支持的编码 ISO-8859-1 引起:

StringHttpMessageConverter

/**
 * Implementation of {@link HttpMessageConverter} that can read and write strings.
 *
 * <p>By default, this converter supports all media types ({@code }),
 * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden
 * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 */
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

 public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;

 /**
  * A default constructor that uses {@code "ISO-8859-1"} as the default charset.
  * @see #StringHttpMessageConverter(Charset)
  */
 public StringHttpMessageConverter() {
  this(DEFAULT_CHARSET);
 }

}

如果在使用过程中发生乱码,我们可以通过方法设置HttpMessageConverter支持的编码,常用的有UTF-8、GBK等。

4、反序列化异常

这是开发过程中容易碰到的又一个问题。因为Java的开源框架和工具类非常之多,而且版本更迭频繁,所以经常发生一些意想不到的坑。

以joda time为例,joda time是流行的java时间和日期框架,但是如果你的接口对外暴露joda time的类型,比如DateTime,那么接口调用方(同构和异构系统)可能会碰到序列化难题,反序列化时甚至直接抛出如下异常:

org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.joda.time.Chronology]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.joda.time.Chronology` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (PushbackInputStream);

我在前厂就碰到过,后来为了调用方便,改回直接暴露Java的Date类型。

当然解决的方案不止这一种,可以使用jackson支持自定义类的序列化和反序列化的方式。在精度要求不是很高的系统里,实现简单的DateTime自定义序列化:

DateTimeSerializer

package com.power.demo.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;

/**
 * 在默认情况下,jackson会将joda time序列化为较为复杂的形式,不利于阅读,并且对象较大。
 * <p>
 * JodaTime 序列化的时候可以将datetime序列化为字符串,更容易读
 **/
public class DateTimeSerializer extends JsonSerializer<DateTime> {

 private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");

 @Override
 public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
  jgen.writeString(value.toString(dateFormatter));
 }
}

以及DateTime反序列化:

DatetimeDeserializer

package com.power.demo.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;

/**
 * JodaTime 反序列化将字符串转化为datetime
 **/
public class DatetimeDeserializer extends JsonDeserializer<DateTime> {

 private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss");

 @Override
 public DateTime deserialize(JsonParser jp, DeserializationContext context) throws IOException, JsonProcessingException {
  JsonNode node = jp.getCodec().readTree(jp);
  String s = node.asText();
  DateTime parse = DateTime.parse(s, dateFormatter);
  return parse;
 }
}

最后可以在RestTemplateConfig类中对常见调用问题进行汇总处理,可以参考如下:

RestTemplateConfig

package com.power.demo.restclient.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.Lists;
import com.power.demo.util.DateTimeSerializer;
import com.power.demo.util.DatetimeDeserializer;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Component
public class RestTemplateConfig {

 // 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
 @Autowired
 private RestTemplateBuilder builder;

 @Autowired
 private ObjectMapper objectMapper;

 // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
 @Bean
 public RestTemplate restTemplate() {

  RestTemplate restTemplate = builder.build();

  //注册model,用于实现jackson joda time序列化和反序列化
  SimpleModule module = new SimpleModule();
  module.addSerializer(DateTime.class, new DateTimeSerializer());
  module.addDeserializer(DateTime.class, new DatetimeDeserializer());
  objectMapper.registerModule(module);

  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  converter.setObjectMapper(objectMapper);

  //不加会出现异常
  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]
  MediaType[] mediaTypes = new MediaType[]{
    MediaType.APPLICATION_JSON,
    MediaType.APPLICATION_OCTET_STREAM,

    MediaType.TEXT_HTML,
    MediaType.TEXT_PLAIN,
    MediaType.TEXT_XML,
    MediaType.APPLICATION_STREAM_JSON,
    MediaType.APPLICATION_ATOM_XML,
    MediaType.APPLICATION_FORM_URLENCODED,
    MediaType.APPLICATION_JSON_UTF8,
    MediaType.APPLICATION_PDF,
  };

  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

  try {
   //通过反射设置MessageConverters
   Field field = restTemplate.getClass().getDeclaredField("messageConverters");

   field.setAccessible(true);

   List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate);

   Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream()
     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
       .getName()))
     .findFirst();

   if (opConverter.isPresent() == false) {
    return restTemplate;
   }

   messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter

   //添加原有的剩余的HttpMessageConverter
   List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream()
     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
       .getName()) == false)
     .collect(Collectors.toList());

   messageConverters.addAll(leftConverters);

   System.out.println(String.format("【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"
     , orgConverterList.size(), messageConverters.size()));

  } catch (Exception e) {
   e.printStackTrace();
  }

  restTemplate.setMessageConverters(messageConverters);

  return restTemplate;
 }
}

目前良好地解决了RestTemplate常用调用问题,而且不需要你写RestTemplate帮助工具类了。

上面列举的这些常见问题,其实.NET下面也有,有兴趣大家可以搜索一下微软的HttpClient常见使用问题,用过的人都深有体会。更不用提 RestSharp 这个开源类库,几年前用的过程中发现了非常多的Bug,到现在还有一个反序列化数组的问题困扰着我们,我只好自己造个简单轮子特殊处理,给我最深刻的经验就是,很多看上去简单的功能,真的碰到了依然会花掉不少的时间去排查和解决,甚至要翻看源码。所以,我们写代码要认识到,越是通用的工具,越需要考虑到特例,可能你需要花80%以上的精力去处理20%的特殊情况,这估计也是满足常见的二八定律吧。

参考:

https://stackoverflow.com/questions/21854369/no-suitable-httpmessageconverter-found-for-response-type

https://stackoverflow.com/questions/40726145/rest-templatecould-not-extract-response-no-suitable-httpmessageconverter-found

https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter

http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 如何使用Spring RestTemplate访问restful服务

    一. 什么是RestTemplate Spring's central class for synchronous client-side HTTP access. It simplifies communication with HTTP servers, and enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs(with possible tem

  • Spring Boot使用RestTemplate消费REST服务的几个问题记录

    我们可以通过Spring Boot快速开发REST接口,同时也可能需要在实现接口的过程中,通过Spring Boot调用内外部REST接口完成业务逻辑. 在Spring Boot中,调用REST Api常见的一般主要有两种方式,通过自带的RestTemplate或者自己开发http客户端工具实现服务调用. RestTemplate基本功能非常强大,不过某些特殊场景,我们可能还是更习惯用自己封装的工具类,比如上传文件至分布式文件系统.处理带证书的https请求等. 本文以RestTemplate来

  • Spring Boot搭建文件上传服务的方法

    本文实例为大家分享了Spring Boot搭建文件上传服务的具体代码,供大家参考,具体内容如下 一.服务端 pom.xml <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http:/

  • Spring Boot和Docker实现微服务部署的方法

    Spring boot 开发轻巧的微服务提供了便利,Docker 的发展又极大的方便了微服务的部署.这篇文章介绍一下如果借助 maven 来快速的生成微服务的镜像以及快速启动服务. 其实将 Spring Boot 项目用 Docker 部署也不是什么多么神秘的技术,也要先生成镜像,再用镜像启动容器,如果说有什么方便的地方,也就是一些工具可以帮助我们节省手动操作的过程. 知识背景: 掌握 docker 的安装以及基本的操作,熟悉 Dockerfile 文件创建镜像的方法. 创建 Spring bo

  • spring boot使用WebClient调用HTTP服务代码示例

    这篇文章主要介绍了spring boot使用WebClient调用HTTP服务代码示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 WebClient的请求模式属于异步非阻塞,能够以少量固定的线程处理高并发的HTTP请求 WebClient是Spring WebFlux模块提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具,从Spring5.0开始提供 在Spring Boot应用中 1.添加Spring WebFlux依赖 <d

  • 使用Spring Boot 2.x构建Web服务的详细代码

    目录 架构: 库: 运行应用的步骤 关于项目配置 Web服务声明 示例 通用错误处理 示例 Spring Data(JPA)配置 Entity类 Repository接口 在application.properties中的JPA数据库配置 数据库配置 application.properties application-dev.properties application-pro.properties 数据库密码加密 SampleWebservice.java JWT身份验证配置 创建令牌的过程

  • 详解Spring boot Admin 使用eureka监控服务

    前言 最近刚好有空,来学习一下如何搭建spring boot admin环境.其中遇到很多的坑. 网上大多都是使用admin-url的方式直接来监控的,感觉一点也不灵活,这不是我想要的结果,所以本篇介绍借助eureka服务注册和发现功能来灵活监控程序. 本文主要记录spring boot admin的搭建过程,希望能有所帮助.其实非常的简单,不要被使用常规方式的误导! 环境介绍 IDE:intellij idea jdk: java8 maven:3.3.9 spring boot:1.5.6

  • Spring Boot 2.0快速构建服务组件全步骤

    前言 所谓的服务组件(Service Component)- 就是用于处理系统业务逻辑的类,如果按照系统分层设计理论来划分,服务组件是位于业务层当中的类.在Spring Boot中,服务组件是一个被**@Service**注解进行注释的类,这些类用于编写系统的业务代码.在本章节中,将讲解如何创建并使用服务组件. 在开始正文之前,先来看两段示例代码.使用服务组件之前,我们需要定义服务组件接口类,用于索引服务组件提供的服务,代码如下所示: public interface UserService{

  • spring boot配置多个请求服务代理的完整步骤

    springboot 配置服务代理 有时候,我们可能有下边这样的需求: 即,针对于分布式服务,我们会有多种业务接口服务,但是服务器上可能只要求开放一个服务的端口,比如上图的restA项目端口是对外开放的,但是restB项目端口并未对外开放,这样带来的问题就是,用户无法直接请求restB项目. 那就想到了可以通过访问restA ,请求路径符合一定规范的时候,比如http://ip:port/test ,当请求中以rest 开头时,可以再转发请求到restB 项目中即可. 当然代理转发 ,有很多的解

  • Java Spring Boot消息服务万字详解分析

    目录 消息服务概述 为什么要使用消息服务 异步处理 应用解耦 流量削峰 分布式事务管理 常用消息中间件介绍 ActiveMQ RabbitMQ RocketMQ RabbitMQ消息中间件 RabbitMQ简介 RabbitMQ工作模式介绍 Work queues(工作队列模式) Public/Subscribe(发布订阅模式) Routing(路由模式) Topics(通配符模式) RPC Headers RabbitMQ安装以及整合环境搭建 安装RabbitMQ 下载RabbitMQ 安装R

  • 使用Spring Boot+gRPC构建微服务并部署的案例详解

    目录 1.  为什么要用Istio? 1.1.  为什么要使用gRPC作为通信框架? 1.2. 编写业务代码 1.3、 编写Dockerfile 2. 编写部署文件 3. 部署应用到Istio 大家好,搞微服务也有好几年时间,从16年开始就一直关注微服务,到现在一直在使用的还是SpringCloud原生那套,虽然后来出现了SpringCloud Alibaba,但由于前面的所有系统框架都已定,就没有在变化,而在微服务的实施过程,为了降运维的服务度,先后使用了jenkins,docker, kub

随机推荐