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):
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 方法的实现如下:
/** * 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, </span>"'url' must not be null"<span style="color: rgba(0, 0, 0, 1)">); Assert.notNull(method, </span>"'method' must not be null"<span style="color: rgba(0, 0, 0, 1)">); ClientHttpResponse response </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { ClientHttpRequest request </span>=<span style="color: rgba(0, 0, 0, 1)"> createRequest(url, method); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestCallback != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { requestCallback.doWithRequest(request); } response </span>=<span style="color: rgba(0, 0, 0, 1)"> request.execute(); handleResponse(url, method, response); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (responseExtractor != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> responseExtractor.extractData(response); } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException ex) { String resource </span>=<span style="color: rgba(0, 0, 0, 1)"> url.toString(); String query </span>=<span style="color: rgba(0, 0, 0, 1)"> url.getRawQuery(); resource </span>= (query != <span style="color: rgba(0, 0, 255, 1)">null</span> ? resource.substring(0, resource.indexOf('?'<span style="color: rgba(0, 0, 0, 1)">)) : resource); </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ResourceAccessException("I/O error on " + method.name() + " request for \"" + resource + "\": " +<span style="color: rgba(0, 0, 0, 1)"> ex.getMessage(), ex); } </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (response != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { response.close(); } } }</span></pre>
doExecute 方法封装了模板方法,比如创建连接、处理请求和应答,关闭连接等。
多数人看到这里,估计都会觉得封装一个 RestClient 不过如此吧?
3、简单调用
以一个 POST 调用为例:
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 </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { response </span>= restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) { e.printStackTrace(); response </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ClientGetGoodsByGoodsIdResponse(); response.setCode(AppConst.FAIL); response.setMessage(e.toString()); } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> response;
}
private String getGoodsSvcUrl() {
String url </span>= ""<span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { _serviceUrl </span>= ""<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl.length() == 0<span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> url; } </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/"<span style="color: rgba(0, 0, 0, 1)">) { url </span>= String.format("%sapi/v1/goods"<span style="color: rgba(0, 0, 0, 1)">, _serviceUrl); } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { url </span>= String.format("%s/api/v1/goods"<span style="color: rgba(0, 0, 0, 1)">, _serviceUrl); } </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> url;
}
}
demo 里直接 RestTemplate.postForObject 方法调用,反序列化实体转换这些 RestTemplate 内部封装搞定。
二、问题汇总
1、no suitable HttpMessageConverter found for request type 异常
这个问题通常会出现在 postForObject 中传入对象进行调用的时候。
分析 RestTemplate 源码,在 HttpEntityRequestCallback 类的 doWithRequest 方法中,如果messageConverters(这个字段后面会继续提及)列表字段循环处理的过程中没有满足 return 跳出的逻辑(也就是没有匹配的 HttpMessageConverter),则抛出上述异常:
@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); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">; } } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (messageConverter.canWrite(requestBodyClass, requestContentType)) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">requestHeaders.isEmpty()) { </span><span style="color: rgba(0, 0, 255, 1)">for</span> (Map.Entry<String, List<String>><span style="color: rgba(0, 0, 0, 1)"> entry : requestHeaders.entrySet()) { httpHeaders.put(entry.getKey(), </span><span style="color: rgba(0, 0, 255, 1)">new</span> LinkedList<><span style="color: rgba(0, 0, 0, 1)">(entry.getValue())); } } </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestContentType != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { logger.debug(</span>"Writing [" + requestBody + "] as \"" + requestContentType + "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> { logger.debug(</span>"Writing [" + requestBody + "] using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">); } } ((HttpMessageConverter</span><Object><span style="color: rgba(0, 0, 0, 1)">) messageConverter).write( requestBody, requestContentType, httpRequest); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">; } } String message </span>= "Could not write request: no suitable HttpMessageConverter found for request type [" +<span style="color: rgba(0, 0, 0, 1)"> requestBodyClass.getName() </span>+ "]"<span style="color: rgba(0, 0, 0, 1)">; </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestContentType != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { message </span>+= " and content type [" + requestContentType + "]"<span style="color: rgba(0, 0, 0, 1)">; } </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> RestClientException(message); } }</span></pre>
最简单的解决方案是,可以通过包装 http 请求头,并将请求对象序列化成字符串的形式传参,参考示例代码如下:
/* * 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 </span>=<span style="color: rgba(0, 0, 0, 1)"> SerializeUtil.Serialize(params); HttpEntity</span><String> formEntity = <span style="color: rgba(0, 0, 255, 1)">new</span> HttpEntity<String><span style="color: rgba(0, 0, 0, 1)">(json, headers); String result </span>= restTemplate.postForObject(url, formEntity, String.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result; }</span></pre>
如果我们还想直接返回对象,直接反序列化返回的字符串即可:
/* * Post 请求调用 * */ public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) { T response = null;String respStr </span>=<span style="color: rgba(0, 0, 0, 1)"> postForObject(restTemplate, url, params); response </span>=<span style="color: rgba(0, 0, 0, 1)"> SerializeUtil.DeSerialize(respStr, clazz); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> response; }</span></pre>
其中,序列化和反序列化工具比较多,常用的比如 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:
/** * 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 方法有个解析应答及反序列化逻辑,如果不成功,抛出的异常信息和上述一致:
@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);</span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 0, 255, 1)">for</span> (HttpMessageConverter<?> messageConverter : <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.messageConverters) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (messageConverter <span style="color: rgba(0, 0, 255, 1)">instanceof</span><span style="color: rgba(0, 0, 0, 1)"> GenericHttpMessageConverter) { GenericHttpMessageConverter</span><?> genericMessageConverter =<span style="color: rgba(0, 0, 0, 1)"> (GenericHttpMessageConverter</span><?><span style="color: rgba(0, 0, 0, 1)">) messageConverter; </span><span style="color: rgba(0, 0, 255, 1)">if</span> (genericMessageConverter.canRead(<span style="color: rgba(0, 0, 255, 1)">this</span>.responseType, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, contentType)) { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) { logger.debug(</span>"Reading [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] as \"" +<span style="color: rgba(0, 0, 0, 1)"> contentType </span>+ "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">return</span> (T) genericMessageConverter.read(<span style="color: rgba(0, 0, 255, 1)">this</span>.responseType, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, responseWrapper); } } </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.responseClass != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">if</span> (messageConverter.canRead(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseClass, contentType)) { </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) { logger.debug(</span>"Reading [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseClass.getName() + "] as \"" +<span style="color: rgba(0, 0, 0, 1)"> contentType </span>+ "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">); } </span><span style="color: rgba(0, 0, 255, 1)">return</span> (T) messageConverter.read((Class) <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseClass, responseWrapper); } } } } </span><span style="color: rgba(0, 0, 255, 1)">catch</span> (IOException |<span style="color: rgba(0, 0, 0, 1)"> HttpMessageNotReadableException ex) { </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> RestClientException("Error while extracting response for type [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] and content type [" + contentType + "]"<span style="color: rgba(0, 0, 0, 1)">, ex); } </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> RestClientException("Could not extract response: no suitable HttpMessageConverter found " + "for response type [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] and content type [" + contentType + "]"<span style="color: rgba(0, 0, 0, 1)">); }</span></pre>
StackOverflow 上的解决的示例代码可以接受,但是并不准确,常见的 MIMEType 都应该加进去,贴一下我认为正确的代码:
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 {</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed"<span style="color: rgba(0, 0, 0, 1)">, RestTemplate .</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span>.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader()); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>
@Autowired
private RestTemplateBuilder builder;@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>
@Bean
public RestTemplate restTemplate() {MediaType[] mediaTypes = new MediaType[]{ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM,RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build(); List</span><HttpMessageConverter<?>> messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList(); MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加会出现异常 </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span>
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)); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">messageConverters.add(converter);</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2Present) { messageConverters.add(converter); } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (gsonPresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> GsonHttpMessageConverter()); } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jsonbPresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> JsonbHttpMessageConverter()); } messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> FormHttpMessageConverter()); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ByteArrayHttpMessageConverter()); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> StringHttpMessageConverter()); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span> ResourceHttpMessageConverter(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SourceHttpMessageConverter()); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AllEncompassingFormHttpMessageConverter()); </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (romePresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AtomFeedHttpMessageConverter()); messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> RssChannelHttpMessageConverter()); } </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2XmlPresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2XmlHttpMessageConverter()); } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jaxb2Present) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Jaxb2RootElementHttpMessageConverter()); } </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2SmilePresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2SmileHttpMessageConverter()); } </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2CborPresent) { messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2CborHttpMessageConverter()); } restTemplate.setMessageConverters(messageConverters); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate; }
}
看到上面的代码,再对比一下 RestTemplate 内部实现,就知道我参考了 RestTemplate 的源码,有洁癖的人可能会说这一坨代码有点啰嗦,上面那一堆 static final 的变量和 messageConverters 填充数据方法,暴露了 RestTemplate 的实现,如果 RestTemplate 修改了,这里也要改,非常不友好,而且看上去一点也不 OO。
经过分析,RestTemplateBuilder.build() 构造了 RestTemplate 对象,只要将内部 MappingJackson2HttpMessageConverter 修改一下支持的 MediaType 即可,RestTemplate 的 messageConverters 字段虽然是 private final 的,我们依然可以通过反射修改之,改进后的代码如下:
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 {</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>
@Autowired
private RestTemplateBuilder builder;@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>
@Bean
public RestTemplate restTemplate() {MediaType[] mediaTypes = new MediaType[]{ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM,RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build(); List</span><HttpMessageConverter<?>> messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList(); MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加可能会出现异常 </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span>
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)); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">通过反射设置MessageConverters</span> Field field = restTemplate.getClass().getDeclaredField("messageConverters"<span style="color: rgba(0, 0, 0, 1)">); field.setAccessible(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">); List</span><HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>><span style="color: rgba(0, 0, 0, 1)">) field.get(restTemplate); Optional</span><HttpMessageConverter<?>> opConverter =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream() .filter(x </span>-> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> .getName())) .findFirst(); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (opConverter.isPresent() == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate; } messageConverters.add(converter);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加MappingJackson2HttpMessageConverter </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加原有的剩余的HttpMessageConverter</span> List<HttpMessageConverter<?>> leftConverters =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream() .filter(x </span>-> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> .getName()) </span>== <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) .collect(Collectors.toList()); messageConverters.addAll(leftConverters); System.out.println(String.format(</span>"【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"<span style="color: rgba(0, 0, 0, 1)"> , orgConverterList.size(), messageConverters.size())); } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) { e.printStackTrace(); } restTemplate.setMessageConverters(messageConverters); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate; }
}
除了一个 messageConverters 字段,看上去我们不再关心 RestTemplate 那些外部依赖包和内部构造过程,果然干净简洁好维护了很多。
3、乱码问题
这个也是一个非常经典的问题。解决方案非常简单,找到 HttpMessageConverter,看看默认支持的 Charset。AbstractJackson2HttpMessageConverter 是很多 HttpMessageConverter 的基类,默认编码为 UTF-8:
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> Charset DEFAULT_CHARSET =<span style="color: rgba(0, 0, 0, 1)"> StandardCharsets.UTF_8;
}
而 StringHttpMessageConverter 比较特殊,有人反馈过发生乱码问题由它默认支持的编码ISO-8859-1引起:
/** * 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> {</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> Charset DEFAULT_CHARSET =<span style="color: rgba(0, 0, 0, 1)"> StandardCharsets.ISO_8859_1; </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)"> * A default constructor that uses {</span><span style="color: rgba(128, 128, 128, 1)">@code</span><span style="color: rgba(0, 128, 0, 1)"> "ISO-8859-1"} as the default charset. * </span><span style="color: rgba(128, 128, 128, 1)">@see</span><span style="color: rgba(0, 128, 0, 1)"> #StringHttpMessageConverter(Charset) </span><span style="color: rgba(0, 128, 0, 1)">*/</span> <span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> StringHttpMessageConverter() { </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">(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 自定义序列化:
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🇲🇲ss");
@Override
public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeString(value.toString(dateFormatter));
}
}
以及 DateTime 反序列化:
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🇲🇲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 类中对常见调用问题进行汇总处理,可以参考如下:
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 {</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>
@Autowired
private RestTemplateBuilder builder;@Autowired </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper; </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>
@Bean
public RestTemplate restTemplate() {RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build(); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">注册model,用于实现jackson joda time序列化和反序列化</span> SimpleModule module = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SimpleModule(); module.addSerializer(DateTime.</span><span style="color: rgba(0, 0, 255, 1)">class</span>, <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> DateTimeSerializer()); module.addDeserializer(DateTime.</span><span style="color: rgba(0, 0, 255, 1)">class</span>, <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> DatetimeDeserializer()); objectMapper.registerModule(module); List</span><HttpMessageConverter<?>> messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList(); MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加会出现异常 </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span> MediaType[] mediaTypes = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> 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)); </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">通过反射设置MessageConverters</span> Field field = restTemplate.getClass().getDeclaredField("messageConverters"<span style="color: rgba(0, 0, 0, 1)">); field.setAccessible(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">); List</span><HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>><span style="color: rgba(0, 0, 0, 1)">) field.get(restTemplate); Optional</span><HttpMessageConverter<?>> opConverter =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream() .filter(x </span>-> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> .getName())) .findFirst(); </span><span style="color: rgba(0, 0, 255, 1)">if</span> (opConverter.isPresent() == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) { </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate; } messageConverters.add(converter);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加MappingJackson2HttpMessageConverter </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加原有的剩余的HttpMessageConverter</span> List<HttpMessageConverter<?>> leftConverters =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream() .filter(x </span>-> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> .getName()) </span>== <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) .collect(Collectors.toList()); messageConverters.addAll(leftConverters); System.out.println(String.format(</span>"【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"<span style="color: rgba(0, 0, 0, 1)"> , orgConverterList.size(), messageConverters.size())); } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) { e.printStackTrace(); } restTemplate.setMessageConverters(messageConverters); </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate; }
}
目前良好地解决了 RestTemplate 常见调用问题,而且不需要你写 RestTemplate 帮助工具类了。
上面列举的这些常见问题,其实.NET 下面也有,有兴趣大家可以搜索一下微软的 HttpClient 常见使用问题,用过的人都深有体会。更不用提RestSharp这个开源类库,几年前用的过程中发现了非常多的 Bug,到现在还有一个反序列化数组的问题困扰着我们,我只好自己造个简单轮子特殊处理,给我最深刻的经验就是,很多看上去简单的功能,真的碰到了依然会花掉不少的时间去排查和解决,甚至要翻看源码。所以,我们写代码要认识到,越是通用的工具,越需要考虑到特例,可能你需要花 80% 以上的精力去处理 20% 的特殊情况,这估计也是满足常见的二八定律吧。
参考:
https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter
http://www.cnblogs.com/rollenholt/p/3934649.html
http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found