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):

  1. public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
  2.  
  3. public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
  4.  
  5. public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables)
  6.  
  7. public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)
  8.  
  9. public void delete(String url, Object... uriVariables)
  10.  
  11. 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

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

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

3、简单调用

以一个POST调用为例:

  1. package com.power.demo.restclient;
  2.  
  3. import com.power.demo.common.AppConst;
  4. import com.power.demo.restclient.clientrequest.ClientGetGoodsByGoodsIdRequest;
  5. import com.power.demo.restclient.clientresponse.ClientGetGoodsByGoodsIdResponse;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.beans.factory.annotation.Value;
  8. import org.springframework.stereotype.Component;
  9. import org.springframework.web.client.RestTemplate;
  10.  
  11. /**
  12. * 商品REST接口客户端 (demo测试用)
  13. **/
  14. @Component
  15. public class GoodsServiceClient {
  16.  
  17. //服务消费者调用的接口URL 形如:http://localhost:9090
  18. @Value("${spring.power.serviceurl}")
  19. private String _serviceUrl;
  20.  
  21. @Autowired
  22. private RestTemplate restTemplate;
  23.  
  24. public ClientGetGoodsByGoodsIdResponse getGoodsByGoodsId(ClientGetGoodsByGoodsIdRequest request) {
  25. String svcUrl = getGoodsSvcUrl() + "/getinfobyid";
  26.  
  27. ClientGetGoodsByGoodsIdResponse response = null;
  28.  
  29. try {
  30. response = restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.class);
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. response = new ClientGetGoodsByGoodsIdResponse();
  34. response.setCode(AppConst.FAIL);
  35. response.setMessage(e.toString());
  36. }
  37.  
  38. return response;
  39. }
  40.  
  41. private String getGoodsSvcUrl() {
  42.  
  43. String url = "";
  44.  
  45. if (_serviceUrl == null) {
  46. _serviceUrl = "";
  47. }
  48. if (_serviceUrl.length() == 0) {
  49. return url;
  50. }
  51.  
  52. if (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/") {
  53. url = String.format("%sapi/v1/goods", _serviceUrl);
  54. } else {
  55. url = String.format("%s/api/v1/goods", _serviceUrl);
  56. }
  57.  
  58. return url;
  59. }
  60.  
  61. }

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

二、问题汇总

1、no suitable HttpMessageConverter found for request type异常

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

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

  1. @Override
  2. @SuppressWarnings("unchecked")
  3. public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
  4. super.doWithRequest(httpRequest);
  5. Object requestBody = this.requestEntity.getBody();
  6. if (requestBody == null) {
  7. HttpHeaders httpHeaders = httpRequest.getHeaders();
  8. HttpHeaders requestHeaders = this.requestEntity.getHeaders();
  9. if (!requestHeaders.isEmpty()) {
  10. for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
  11. httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
  12. }
  13. }
  14. if (httpHeaders.getContentLength() < 0) {
  15. httpHeaders.setContentLength(0L);
  16. }
  17. }
  18. else {
  19. Class<?> requestBodyClass = requestBody.getClass();
  20. Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
  21. ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
  22. HttpHeaders httpHeaders = httpRequest.getHeaders();
  23. HttpHeaders requestHeaders = this.requestEntity.getHeaders();
  24. MediaType requestContentType = requestHeaders.getContentType();
  25. for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
  26. if (messageConverter instanceof GenericHttpMessageConverter) {
  27. GenericHttpMessageConverter<Object> genericConverter =
  28. (GenericHttpMessageConverter<Object>) messageConverter;
  29. if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
  30. if (!requestHeaders.isEmpty()) {
  31. for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
  32. httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
  33. }
  34. }
  35. if (logger.isDebugEnabled()) {
  36. if (requestContentType != null) {
  37. logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
  38. "\" using [" + messageConverter + "]");
  39. }
  40. else {
  41. logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
  42. }
  43.  
  44. }
  45. genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
  46. return;
  47. }
  48. }
  49. else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
  50. if (!requestHeaders.isEmpty()) {
  51. for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
  52. httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
  53. }
  54. }
  55. if (logger.isDebugEnabled()) {
  56. if (requestContentType != null) {
  57. logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
  58. "\" using [" + messageConverter + "]");
  59. }
  60. else {
  61. logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
  62. }
  63.  
  64. }
  65. ((HttpMessageConverter<Object>) messageConverter).write(
  66. requestBody, requestContentType, httpRequest);
  67. return;
  68. }
  69. }
  70. String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
  71. requestBodyClass.getName() + "]";
  72. if (requestContentType != null) {
  73. message += " and content type [" + requestContentType + "]";
  74. }
  75. throw new RestClientException(message);
  76. }
  77. }

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

按 Ctrl+C 复制代码
按 Ctrl+C 复制代码

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

  1. /*
  2. * Post请求调用
  3. * */
  4. public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) {
  5. T response = null;
  6.  
  7. String respStr = postForObject(restTemplate, url, params);
  8.  
  9. response = SerializeUtil.DeSerialize(respStr, clazz);
  10.  
  11. return response;
  12. }

其中,序列化和反序列化工具比较多,常用的比如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

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

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

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

  1. String url = "http://wthrcdn.etouch.cn/weather_mini?city=上海";
  2.  
  3. 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方法有个解析应答及反序列化逻辑,如果不成功,抛出的异常信息和上述一致:

  1. @Override
  2. @SuppressWarnings({"unchecked", "rawtypes", "resource"})
  3. public T extractData(ClientHttpResponse response) throws IOException {
  4. MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
  5. if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
  6. return null;
  7. }
  8. MediaType contentType = getContentType(responseWrapper);
  9.  
  10. try {
  11. for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
  12. if (messageConverter instanceof GenericHttpMessageConverter) {
  13. GenericHttpMessageConverter<?> genericMessageConverter =
  14. (GenericHttpMessageConverter<?>) messageConverter;
  15. if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
  16. if (logger.isDebugEnabled()) {
  17. logger.debug("Reading [" + this.responseType + "] as \"" +
  18. contentType + "\" using [" + messageConverter + "]");
  19. }
  20. return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
  21. }
  22. }
  23. if (this.responseClass != null) {
  24. if (messageConverter.canRead(this.responseClass, contentType)) {
  25. if (logger.isDebugEnabled()) {
  26. logger.debug("Reading [" + this.responseClass.getName() + "] as \"" +
  27. contentType + "\" using [" + messageConverter + "]");
  28. }
  29. return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
  30. }
  31. }
  32. }
  33. }
  34. catch (IOException | HttpMessageNotReadableException ex) {
  35. throw new RestClientException("Error while extracting response for type [" +
  36. this.responseType + "] and content type [" + contentType + "]", ex);
  37. }
  38.  
  39. throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " +
  40. "for response type [" + this.responseType + "] and content type [" + contentType + "]");
  41. }

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

  1. package com.power.demo.restclient.config;
  2.  
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.google.common.collect.Lists;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.web.client.RestTemplateBuilder;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.http.MediaType;
  9. import org.springframework.http.converter.*;
  10. import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
  11. import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
  12. import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
  13. import org.springframework.http.converter.json.GsonHttpMessageConverter;
  14. import org.springframework.http.converter.json.JsonbHttpMessageConverter;
  15. import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  16. import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
  17. import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
  18. import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
  19. import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
  20. import org.springframework.http.converter.xml.SourceHttpMessageConverter;
  21. import org.springframework.stereotype.Component;
  22. import org.springframework.util.ClassUtils;
  23. import org.springframework.web.client.RestTemplate;
  24.  
  25. import java.util.Arrays;
  26. import java.util.List;
  27.  
  28. @Component
  29. public class RestTemplateConfig {
  30.  
  31. private static final boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", RestTemplate
  32. .class.getClassLoader());
  33. private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader());
  34. 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());
  35. private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader());
  36. private static final boolean jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.class.getClassLoader());
  37. private static final boolean jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.class.getClassLoader());
  38. private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader());
  39. private static final boolean jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.class.getClassLoader());
  40.  
  41. // 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
  42. @Autowired
  43. private RestTemplateBuilder builder;
  44.  
  45. @Autowired
  46. private ObjectMapper objectMapper;
  47.  
  48. // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
  49. @Bean
  50. public RestTemplate restTemplate() {
  51.  
  52. RestTemplate restTemplate = builder.build();
  53.  
  54. List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
  55. MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  56. converter.setObjectMapper(objectMapper);
  57.  
  58. //不加会出现异常
  59. //Could not extract response: no suitable HttpMessageConverter found for response type [class ]
  60.  
  61. MediaType[] mediaTypes = new MediaType[]{
  62. MediaType.APPLICATION_JSON,
  63. MediaType.APPLICATION_OCTET_STREAM,
  64.  
  65. MediaType.APPLICATION_JSON_UTF8,
  66. MediaType.TEXT_HTML,
  67. MediaType.TEXT_PLAIN,
  68. MediaType.TEXT_XML,
  69. MediaType.APPLICATION_STREAM_JSON,
  70. MediaType.APPLICATION_ATOM_XML,
  71. MediaType.APPLICATION_FORM_URLENCODED,
  72. MediaType.APPLICATION_PDF,
  73. };
  74.  
  75. converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));
  76.  
  77. //messageConverters.add(converter);
  78. if (jackson2Present) {
  79. messageConverters.add(converter);
  80. } else if (gsonPresent) {
  81. messageConverters.add(new GsonHttpMessageConverter());
  82. } else if (jsonbPresent) {
  83. messageConverters.add(new JsonbHttpMessageConverter());
  84. }
  85.  
  86. messageConverters.add(new FormHttpMessageConverter());
  87.  
  88. messageConverters.add(new ByteArrayHttpMessageConverter());
  89. messageConverters.add(new StringHttpMessageConverter());
  90. messageConverters.add(new ResourceHttpMessageConverter(false));
  91. messageConverters.add(new SourceHttpMessageConverter());
  92. messageConverters.add(new AllEncompassingFormHttpMessageConverter());
  93. if (romePresent) {
  94. messageConverters.add(new AtomFeedHttpMessageConverter());
  95. messageConverters.add(new RssChannelHttpMessageConverter());
  96. }
  97.  
  98. if (jackson2XmlPresent) {
  99. messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
  100. } else if (jaxb2Present) {
  101. messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
  102. }
  103.  
  104. if (jackson2SmilePresent) {
  105. messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
  106. }
  107.  
  108. if (jackson2CborPresent) {
  109. messageConverters.add(new MappingJackson2CborHttpMessageConverter());
  110. }
  111.  
  112. restTemplate.setMessageConverters(messageConverters);
  113.  
  114. return restTemplate;
  115. }
  116.  
  117. }

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

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

  1. package com.power.demo.restclient.config;
  2.  
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.google.common.collect.Lists;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.web.client.RestTemplateBuilder;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.http.MediaType;
  9. import org.springframework.http.converter.HttpMessageConverter;
  10. import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
  11. import org.springframework.stereotype.Component;
  12. import org.springframework.web.client.RestTemplate;
  13.  
  14. import java.lang.reflect.Field;
  15. import java.util.Arrays;
  16. import java.util.List;
  17. import java.util.Optional;
  18. import java.util.stream.Collectors;
  19.  
  20. @Component
  21. public class RestTemplateConfig {
  22.  
  23. // 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例
  24. @Autowired
  25. private RestTemplateBuilder builder;
  26.  
  27. @Autowired
  28. private ObjectMapper objectMapper;
  29.  
  30. // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例
  31. @Bean
  32. public RestTemplate restTemplate() {
  33.  
  34. RestTemplate restTemplate = builder.build();
  35.  
  36. List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
  37. MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  38. converter.setObjectMapper(objectMapper);
  39.  
  40. //不加可能会出现异常
  41. //Could not extract response: no suitable HttpMessageConverter found for response type [class ]
  42.  
  43. MediaType[] mediaTypes = new MediaType[]{
  44. MediaType.APPLICATION_JSON,
  45. MediaType.APPLICATION_OCTET_STREAM,
  46.  
  47. MediaType.TEXT_HTML,
  48. MediaType.TEXT_PLAIN,
  49. MediaType.TEXT_XML,
  50. MediaType.APPLICATION_STREAM_JSON,
  51. MediaType.APPLICATION_ATOM_XML,
  52. MediaType.APPLICATION_FORM_URLENCODED,
  53. MediaType.APPLICATION_JSON_UTF8,
  54. MediaType.APPLICATION_PDF,
  55. };
  56.  
  57. converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));
  58.  
  59. try {
  60. //通过反射设置MessageConverters
  61. Field field = restTemplate.getClass().getDeclaredField("messageConverters");
  62.  
  63. field.setAccessible(true);
  64.  
  65. List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate);
  66.  
  67. Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream()
  68. .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
  69. .getName()))
  70. .findFirst();
  71.  
  72. if (opConverter.isPresent() == false) {
  73. return restTemplate;
  74. }
  75.  
  76. messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter
  77.  
  78. //添加原有的剩余的HttpMessageConverter
  79. List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream()
  80. .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class
  81. .getName()) == false)
  82. .collect(Collectors.toList());
  83.  
  84. messageConverters.addAll(leftConverters);
  85.  
  86. System.out.println(String.format("【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"
  87. , orgConverterList.size(), messageConverters.size()));
  88.  
  89. } catch (Exception e) {
  90. e.printStackTrace();
  91. }
  92.  
  93. restTemplate.setMessageConverters(messageConverters);
  94.  
  95. return restTemplate;
  96. }
  97.  
  98. }

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

3、乱码问题

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

AbstractJackson2HttpMessageConverter

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

StringHttpMessageConverter

如果在使用过程中发生乱码,我们可以通过方法设置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

以及DateTime反序列化:

DatetimeDeserializer

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

RestTemplateConfig

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

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

参考:

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

RestTemplate 使用中的几个问题的更多相关文章

  1. Spring Boot(三):RestTemplate提交表单数据的三种方法

    http://blog.csdn.net/yiifaa/article/details/77939282 ********************************************** ...

  2. RestTemplate使用详解

    1.RestTemplate添加RequestHeader如content-type可通过httpclient设置 List<Header> headers = new ArrayList ...

  3. HttpClient的替代者 - RestTemplate

    需要的包 ,除了Spring的基础包外还用到json的包,这里的数据传输使用json格式 客户端和服务端都用到一下的包 <!-- Spring --> <dependency> ...

  4. RestTemplate发送请求并携带header信息

    1.使用restTemplate的postForObject方法 注:目前没有发现发送携带header信息的getForObject方法. HttpHeaders headers = new Http ...

  5. 【Spring-web】RestTemplate源码学习——梳理内部实现过程

    2016-12-28 by 安静的下雪天  http://www.cnblogs.com/quiet-snowy-day/p/6228198.html  提示:使用手机浏览时请注意,图多费流量. 本篇 ...

  6. ORA-02020 : 过多的数据库链接在使用中-Windows环境解决步骤

    一.现象 编译存储过程时报ORA-02020错误. 错误详细信息:ORA-04052在查找远程对象 xx@yy时出错 ORA-00604 : 递归 SQL 级别 1 出现错误 ORA-02020 : ...

  7. Subsonic使用中

    使用中,遇到各种奇葩问题,依依汇总. 1.引用了Subsonic层后,一运行就开始报错,提示未能找到文件!!    //引用后,目标框架可能会被改变,subsonic的默认框架是2.0,请检查框架是否 ...

  8. 【Spring-web】RestTemplate源码学习

     2016-12-22   by 安静的下雪天  http://www.cnblogs.com/quiet-snowy-day/p/6210288.html 前言 在Web开发工作中,有一部分开发任务 ...

  9. mysqldump 备份命令使用中的一些经验总结

    mysqldump的一个小坑(自测) 正文:经常使用接触mysql复制功能的朋友应该对mysqldump命令不陌生吧,鄙人最近也在研究学习这一块的内容,经过几天的测试,发现mysqldump使用中容易 ...

随机推荐

  1. 【LEETCODE】48、数组分类,简单级别,题目:189,217,219,268,283,414

    package y2019.Algorithm.array; import java.util.Arrays; import java.util.Stack; /** * @ClassName Rot ...

  2. TweenMax参数用法中文介绍

    TweenMax 建立在 TweenLite 和 TweenFilterLite 基础之上,因此,又揉合了这二者的功能,使得功能更加的齐备,但是如果说易用性,觉得还是 TweenLite 来得方便一些 ...

  3. nginx1.14.0版本location路径,多级文件目录配置,root与alias的配置区别

    1.多级目录配置 多级目录是指像/html/mypage 等等配置: server { listen 80; server_name localhost; location = /page1/ { # ...

  4. [洛谷P3227][HNOI2013]切糕

    题目大意:有一个$n\times m$的切糕,每一个位置的高度可以在$[1,k]$之间,每个高度有一个代价,要求四联通的两个格子之间高度最多相差$D$,问可行的最小代价.$n,m,k,D\leqsla ...

  5. java.lang.SecurityManager、java.security包

    学习java大概3年多了,一直没有好好研究过java安全相关的问题,总是会看到 SecurityManger sm = System.getSecurityManager(); if(sm!=null ...

  6. Web Api 转

    GET:生到数据列表(默认),或者得到一条实体数据 POST:添加服务端添加一条记录,记录实体为Form对象 PUT:添加或修改服务端的一条记录,记录实体的Form对象,记录主键以GET方式进行传输 ...

  7. python3-使用requests模拟登录网易云音乐

    # -*- coding: utf-8 -*- from Crypto.Cipher import AES import base64 import random import codecs impo ...

  8. rabbitmq使用教程

    检查RabbitMQ运行状态1)打开命令行命令行,进入RabbitMQ的安装目录: cd D:\installs\rabbitmq\rabbitmq_server-3.7.15\sbin2)输入 ra ...

  9. HTML学习摘要3

    DAY 3 浏览器会自动地在标题的前后添加空行 默认情况下,HTML 会自动地在块级元素前后添加一个额外的空行,比如段落.标题元素前后. <hr /> 标签在 HTML 页面中创建水平线. ...

  10. Docker部署Vue

    在服务器上创建一个存放该文件的文件夹,将生成的文件上传到这个文件夹下. 上传的同级目录中创建Dockerfile以及nginx.conf两个文件. # 设置基础镜像 FROM nginx # 定义作者 ...