Spring Boot使用RestTemplate消費(fèi)REST服務(wù)的幾個(gè)問(wèn)題記錄
我們可以通過(guò)Spring Boot快速開發(fā)REST接口,同時(shí)也可能需要在實(shí)現(xiàn)接口的過(guò)程中,通過(guò)Spring Boot調(diào)用內(nèi)外部REST接口完成業(yè)務(wù)邏輯。
在Spring Boot中,調(diào)用REST Api常見的一般主要有兩種方式,通過(guò)自帶的RestTemplate或者自己開發(fā)http客戶端工具實(shí)現(xiàn)服務(wù)調(diào)用。
RestTemplate基本功能非常強(qiáng)大,不過(guò)某些特殊場(chǎng)景,我們可能還是更習(xí)慣用自己封裝的工具類,比如上傳文件至分布式文件系統(tǒng)、處理帶證書的https請(qǐng)求等。
本文以RestTemplate來(lái)舉例,記錄幾個(gè)使用RestTemplate調(diào)用接口過(guò)程中發(fā)現(xiàn)的問(wèn)題和解決方案。
一、RestTemplate簡(jiǎn)介
1、什么是RestTemplate
我們自己封裝的HttpClient,通常都會(huì)有一些模板代碼,比如建立連接,構(gòu)造請(qǐng)求頭和請(qǐng)求體,然后根據(jù)響應(yīng),解析響應(yīng)信息,最后關(guān)閉連接。
RestTemplate是Spring中對(duì)HttpClient的再次封裝,簡(jiǎn)化了發(fā)起HTTP請(qǐng)求以及處理響應(yīng)的過(guò)程,抽象層級(jí)更高,減少消費(fèi)者的模板代碼,使冗余代碼更少。
其實(shí)仔細(xì)想想Spring Boot下的很多XXXTemplate類,它們也提供各種模板方法,只不過(guò)抽象的層次更高,隱藏了更多細(xì)節(jié)而已。
順便提一下,Spring Cloud有一個(gè)聲明式服務(wù)調(diào)用Feign,是基于Netflix Feign實(shí)現(xiàn)的,整合了Spring Cloud Ribbon與 Spring Cloud Hystrix,并且實(shí)現(xiàn)了聲明式的Web服務(wù)客戶端定義方式。
本質(zhì)上Feign是在RestTemplate的基礎(chǔ)上對(duì)其再次封裝,由它來(lái)幫助我們定義和實(shí)現(xiàn)依賴服務(wù)接口的定義。
2、RestTemplate常見方法
常見的REST服務(wù)有很多種請(qǐng)求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate實(shí)現(xiàn)了最常見的方式,用的最多的就是Get和Post了,調(diào)用API可參考源碼,這里列舉幾個(gè)方法定義(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)
同時(shí)要注意兩個(gè)較為“靈活”的方法 exchange 和 execute 。
RestTemplate暴露的exchange與其它接口的不同:
(1)允許調(diào)用者指定HTTP請(qǐng)求的方法(GET,POST,DELETE等)
(2)可以在請(qǐng)求中增加body以及頭信息,其內(nèi)容通過(guò)參數(shù)‘HttpEntity<?>requestEntity'描述
(3)exchange支持‘含參數(shù)的類型'(即泛型類)作為返回類型,該特性通過(guò)‘ParameterizedTypeReference<T>responseType'描述。
RestTemplate所有的GET,POST等等方法,最終調(diào)用的都是execute方法。excute方法的內(nèi)部實(shí)現(xiàn)是將String格式的URI轉(zhuǎn)成了java.net.URI,之后調(diào)用了doExecute方法,doExecute方法的實(shí)現(xiàn)如下:
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方法封裝了模板方法,比如創(chuàng)建連接、處理請(qǐng)求和應(yīng)答,關(guān)閉連接等。
多數(shù)人看到這里,估計(jì)都會(huì)覺得封裝一個(gè)RestClient不過(guò)如此吧?
3、簡(jiǎn)單調(diào)用
以一個(gè)POST調(diào)用為例:
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測(cè)試用)
**/
@Component
public class GoodsServiceClient {
//服務(wù)消費(fèi)者調(diào)用的接口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方法調(diào)用,反序列化實(shí)體轉(zhuǎn)換這些RestTemplate內(nèi)部封裝搞定。
二、問(wèn)題匯總
1、no suitable HttpMessageConverter found for request type異常
這個(gè)問(wèn)題通常會(huì)出現(xiàn)在postForObject中傳入對(duì)象進(jìn)行調(diào)用的時(shí)候。
分析RestTemplate源碼,在HttpEntityRequestCallback類的doWithRequest方法中,如果 messageConverters (這個(gè)字段后面會(huì)繼續(xù)提及)列表字段循環(huán)處理的過(guò)程中沒(méi)有滿足return跳出的邏輯(也就是沒(méi)有匹配的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);
}
}
最簡(jiǎn)單的解決方案是,可以通過(guò)包裝http請(qǐng)求頭,并將請(qǐng)求對(duì)象序列化成字符串的形式傳參,參考示例代碼如下:
postForObject
/*
* Post請(qǐng)求調(diào)用
* */
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;
}
如果我們還想直接返回對(duì)象,直接反序列化返回的字符串即可:
postForObject
/*
* Post請(qǐng)求調(diào)用
* */
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異常
和發(fā)起請(qǐng)求發(fā)生異常一樣,處理應(yīng)答的時(shí)候也會(huì)有問(wèn)題。
StackOverflow上有人問(wèn)過(guò)相同的問(wèn)題,根本原因是HTTP消息轉(zhuǎn)換器HttpMessageConverter缺少 MIME Type ,也就是說(shuō)HTTP在把輸出結(jié)果傳送到客戶端的時(shí)候,客戶端必須啟動(dòng)適當(dāng)?shù)膽?yīng)用程序來(lái)處理這個(gè)輸出文檔,這可以通過(guò)多種MIME(多功能網(wǎng)際郵件擴(kuò)充協(xié)議)Type來(lái)完成。
對(duì)于服務(wù)端應(yīng)答,很多HttpMessageConverter默認(rèn)支持的媒體類型(MIMEType)都不同。StringHttpMessageConverter默認(rèn)支持的則是MediaType.TEXT_PLAIN,SourceHttpMessageConverter默認(rèn)支持的則是MediaType.TEXT_XML,F(xiàn)ormHttpMessageConverter默認(rèn)支持的是MediaType.APPLICATION_FORM_URLENCODED和MediaType.MULTIPART_FORM_DATA,在REST服務(wù)中,我們用到的最多的還是 MappingJackson2HttpMessageConverter ,這是一個(gè)比較通用的轉(zhuǎn)化器(繼承自GenericHttpMessageConverter接口),根據(jù)分析,它默認(rèn)支持的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"));
}
但是有些應(yīng)用接口默認(rèn)的應(yīng)答MIMEType不是application/json,比如我們調(diào)用一個(gè)外部天氣預(yù)報(bào)接口,如果使用RestTemplate的默認(rèn)配置,直接返回一個(gè)字符串應(yīng)答是沒(méi)有問(wèn)題的:
String url = "http://wthrcdn.etouch.cn/weather_mini?city=上海"; String result = restTemplate.getForObject(url, String.class); ClientWeatherResultVO vo = SerializeUtil.DeSerialize(result, ClientWeatherResultVO.class);
但是,如果我們想直接返回一個(gè)實(shí)體對(duì)象:
String url = "http://wthrcdn.etouch.cn/weather_mini?city=上海"; ClientWeatherResultVO weatherResultVO = restTemplate.getForObject(url, ClientWeatherResultVO.class);
則直接報(bào)異常:
Could not extract response: no suitable HttpMessageConverter found for response type [class ]
and content type [application/octet-stream]
很多人碰到過(guò)這個(gè)問(wèn)題,首次碰到估計(jì)大多都比較懵吧,很多接口都是json或者xml或者plain text格式返回的,什么是application/octet-stream?
查看RestTemplate源代碼,一路跟蹤下去會(huì)發(fā)現(xiàn) HttpMessageConverterExtractor 類的extractData方法有個(gè)解析應(yīng)答及反序列化邏輯,如果不成功,拋出的異常信息和上述一致:
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上的解決的示例代碼可以接受,但是并不準(zhǔn)確,常見的MIMEType都應(yīng)該加進(jìn)去,貼一下我認(rèn)為正確的代碼:
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());
// 啟動(dòng)的時(shí)候要注意,由于我們?cè)诜?wù)中注入了RestTemplate,所以啟動(dòng)的時(shí)候需要實(shí)例化該類的一個(gè)實(shí)例
@Autowired
private RestTemplateBuilder builder;
@Autowired
private ObjectMapper objectMapper;
// 使用RestTemplateBuilder來(lái)實(shí)例化RestTemplate對(duì)象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實(shí)例
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = builder.build();
List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
//不加會(huì)出現(xiàn)異常
//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;
}
}
看到上面的代碼,再對(duì)比一下RestTemplate內(nèi)部實(shí)現(xiàn),就知道我參考了RestTemplate的源碼,有潔癖的人可能會(huì)說(shuō)這一坨代碼有點(diǎn)啰嗦,上面那一堆static final的變量和messageConverters填充數(shù)據(jù)方法,暴露了RestTemplate的實(shí)現(xiàn),如果RestTemplate修改了,這里也要改,非常不友好,而且看上去一點(diǎn)也不OO。
經(jīng)過(guò)分析,RestTemplateBuilder.build()構(gòu)造了RestTemplate對(duì)象,只要將內(nèi)部MappingJackson2HttpMessageConverter修改一下支持的MediaType即可,RestTemplate的messageConverters字段雖然是private final的,我們依然可以通過(guò)反射修改之,改進(jìn)后的代碼如下:
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 {
// 啟動(dòng)的時(shí)候要注意,由于我們?cè)诜?wù)中注入了RestTemplate,所以啟動(dòng)的時(shí)候需要實(shí)例化該類的一個(gè)實(shí)例
@Autowired
private RestTemplateBuilder builder;
@Autowired
private ObjectMapper objectMapper;
// 使用RestTemplateBuilder來(lái)實(shí)例化RestTemplate對(duì)象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實(shí)例
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = builder.build();
List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper);
//不加可能會(huì)出現(xiàn)異常
//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 {
//通過(guò)反射設(shè)置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】原有數(shù)量:%s,重新構(gòu)造后數(shù)量:%s"
, orgConverterList.size(), messageConverters.size()));
} catch (Exception e) {
e.printStackTrace();
}
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
}
除了一個(gè)messageConverters字段,看上去我們不再關(guān)心RestTemplate那些外部依賴包和內(nèi)部構(gòu)造過(guò)程,果然干凈簡(jiǎn)潔好維護(hù)了很多。
3、亂碼問(wèn)題
這個(gè)也是一個(gè)非常經(jīng)典的問(wèn)題。解決方案非常簡(jiǎn)單,找到HttpMessageConverter,看看默認(rèn)支持的Charset。AbstractJackson2HttpMessageConverter是很多HttpMessageConverter的基類,默認(rèn)編碼為UTF-8:
AbstractJackson2HttpMessageConverter
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
}
而StringHttpMessageConverter比較特殊,有人反饋過(guò)發(fā)生亂碼問(wèn)題由它默認(rèn)支持的編碼 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);
}
}
如果在使用過(guò)程中發(fā)生亂碼,我們可以通過(guò)方法設(shè)置HttpMessageConverter支持的編碼,常用的有UTF-8、GBK等。
4、反序列化異常
這是開發(fā)過(guò)程中容易碰到的又一個(gè)問(wèn)題。因?yàn)镴ava的開源框架和工具類非常之多,而且版本更迭頻繁,所以經(jīng)常發(fā)生一些意想不到的坑。
以joda time為例,joda time是流行的java時(shí)間和日期框架,但是如果你的接口對(duì)外暴露joda time的類型,比如DateTime,那么接口調(diào)用方(同構(gòu)和異構(gòu)系統(tǒng))可能會(huì)碰到序列化難題,反序列化時(shí)甚至直接拋出如下異常:
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);
我在前廠就碰到過(guò),后來(lái)為了調(diào)用方便,改回直接暴露Java的Date類型。
當(dāng)然解決的方案不止這一種,可以使用jackson支持自定義類的序列化和反序列化的方式。在精度要求不是很高的系統(tǒng)里,實(shí)現(xiàn)簡(jiǎn)單的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;
/**
* 在默認(rèn)情況下,jackson會(huì)將joda time序列化為較為復(fù)雜的形式,不利于閱讀,并且對(duì)象較大。
* <p>
* JodaTime 序列化的時(shí)候可以將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 反序列化將字符串轉(zhuǎn)化為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類中對(duì)常見調(diào)用問(wèn)題進(jìn)行匯總處理,可以參考如下:
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 {
// 啟動(dòng)的時(shí)候要注意,由于我們?cè)诜?wù)中注入了RestTemplate,所以啟動(dòng)的時(shí)候需要實(shí)例化該類的一個(gè)實(shí)例
@Autowired
private RestTemplateBuilder builder;
@Autowired
private ObjectMapper objectMapper;
// 使用RestTemplateBuilder來(lái)實(shí)例化RestTemplate對(duì)象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實(shí)例
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = builder.build();
//注冊(cè)model,用于實(shí)現(xiàn)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);
//不加會(huì)出現(xiàn)異常
//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 {
//通過(guò)反射設(shè)置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】原有數(shù)量:%s,重新構(gòu)造后數(shù)量:%s"
, orgConverterList.size(), messageConverters.size()));
} catch (Exception e) {
e.printStackTrace();
}
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
}
目前良好地解決了RestTemplate常用調(diào)用問(wèn)題,而且不需要你寫RestTemplate幫助工具類了。
上面列舉的這些常見問(wèn)題,其實(shí).NET下面也有,有興趣大家可以搜索一下微軟的HttpClient常見使用問(wèn)題,用過(guò)的人都深有體會(huì)。更不用提 RestSharp 這個(gè)開源類庫(kù),幾年前用的過(guò)程中發(fā)現(xiàn)了非常多的Bug,到現(xiàn)在還有一個(gè)反序列化數(shù)組的問(wèn)題困擾著我們,我只好自己造個(gè)簡(jiǎn)單輪子特殊處理,給我最深刻的經(jīng)驗(yàn)就是,很多看上去簡(jiǎn)單的功能,真的碰到了依然會(huì)花掉不少的時(shí)間去排查和解決,甚至要翻看源碼。所以,我們寫代碼要認(rèn)識(shí)到,越是通用的工具,越需要考慮到特例,可能你需要花80%以上的精力去處理20%的特殊情況,這估計(jì)也是滿足常見的二八定律吧。
參考:
https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter
http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot配置管理Externalized?Configuration深入探究
這篇文章主要介紹了Springboot配置管Externalized?Configuration深入探究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
JsonFormat與@DateTimeFormat注解實(shí)例解析
這篇文章主要介紹了JsonFormat與@DateTimeFormat注解實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
Java 超詳細(xì)講解IO操作字節(jié)流與字符流
本章具體介紹了字節(jié)流、字符流的基本使用方法,圖解穿插代碼實(shí)現(xiàn)。 JAVA從基礎(chǔ)開始講,后續(xù)會(huì)講到JAVA高級(jí),中間會(huì)穿插面試題和項(xiàng)目實(shí)戰(zhàn),希望能給大家?guī)?lái)幫助2022-03-03
在Map中實(shí)現(xiàn)key唯一不重復(fù)操作
這篇文章主要介紹了在Map中實(shí)現(xiàn)key唯一不重復(fù)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
使用RestTemplate調(diào)用RESTful?API的代碼示例
在開發(fā)?Web?應(yīng)用程序時(shí),調(diào)用?RESTful?API?是一個(gè)常見的任務(wù),本文將介紹如何使用?RestTemplate?調(diào)用?RESTful?API,并提供示例代碼,感興趣的同學(xué)可以跟著小編一起來(lái)看看2023-06-06
Java使用C3P0數(shù)據(jù)源鏈接數(shù)據(jù)庫(kù)
這篇文章主要為大家詳細(xì)介紹了Java使用C3P0數(shù)據(jù)源鏈接數(shù)據(jù)庫(kù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
springmvc直接不經(jīng)過(guò)controller訪問(wèn)WEB-INF中的頁(yè)面問(wèn)題
這篇文章主要介紹了springmvc直接不經(jīng)過(guò)controller訪問(wèn)WEB-INF中的頁(yè)面問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
idea中maven本地倉(cāng)庫(kù)jar包打包失敗和無(wú)法引用的問(wèn)題解決
本文主要介紹了idea中maven本地倉(cāng)庫(kù)jar包打包失敗和無(wú)法引用的問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

