我們可以通過Spring Boot快速開發(fā)REST接口,同時也可能需要在實現(xiàn)接口的過程中,通過Spring Boot調(diào)用內(nèi)外部REST接口完成業(yè)務(wù)邏輯。
專業(yè)從事成都網(wǎng)站設(shè)計、成都網(wǎng)站建設(shè),高端網(wǎng)站制作設(shè)計,微信小程序開發(fā),網(wǎng)站推廣的成都做網(wǎng)站的公司。優(yōu)秀技術(shù)團隊竭力真誠服務(wù),采用H5技術(shù)+CSS3前端渲染技術(shù),成都響應(yīng)式網(wǎng)站建設(shè),讓網(wǎng)站在手機、平板、PC、微信下都能呈現(xiàn)。建站過程建立專項小組,與您實時在線互動,隨時提供解決方案,暢聊想法和感受。
在Spring Boot中,調(diào)用REST Api常見的一般主要有兩種方式,通過自帶的RestTemplate或者自己開發(fā)http客戶端工具實現(xiàn)服務(wù)調(diào)用。
RestTemplate基本功能非常強大,不過某些特殊場景,我們可能還是更習(xí)慣用自己封裝的工具類,比如上傳文件至分布式文件系統(tǒng)、處理帶證書的https請求等。
本文以RestTemplate來舉例,記錄幾個使用RestTemplate調(diào)用接口過程中發(fā)現(xiàn)的問題和解決方案。
一、RestTemplate簡介
1、什么是RestTemplate
我們自己封裝的HttpClient,通常都會有一些模板代碼,比如建立連接,構(gòu)造請求頭和請求體,然后根據(jù)響應(yīng),解析響應(yīng)信息,最后關(guān)閉連接。
RestTemplate是Spring中對HttpClient的再次封裝,簡化了發(fā)起HTTP請求以及處理響應(yīng)的過程,抽象層級更高,減少消費者的模板代碼,使冗余代碼更少。
其實仔細(xì)想想Spring Boot下的很多XXXTemplate類,它們也提供各種模板方法,只不過抽象的層次更高,隱藏了更多細(xì)節(jié)而已。
順便提一下,Spring Cloud有一個聲明式服務(wù)調(diào)用Feign,是基于Netflix Feign實現(xiàn)的,整合了Spring Cloud Ribbon與 Spring Cloud Hystrix,并且實現(xiàn)了聲明式的Web服務(wù)客戶端定義方式。
本質(zhì)上Feign是在RestTemplate的基礎(chǔ)上對其再次封裝,由它來幫助我們定義和實現(xiàn)依賴服務(wù)接口的定義。
2、RestTemplate常見方法
常見的REST服務(wù)有很多種請求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate實現(xiàn)了最常見的方式,用的最多的就是Get和Post了,調(diào)用API可參考源碼,這里列舉幾個方法定義(GET、POST、DELETE):
methods
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables) public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables) public void delete(String url, Object... uriVariables) public void delete(URI url)
同時要注意兩個較為“靈活”的方法 exchange 和 execute 。
RestTemplate暴露的exchange與其它接口的不同:
(1)允許調(diào)用者指定HTTP請求的方法(GET,POST,DELETE等)
(2)可以在請求中增加body以及頭信息,其內(nèi)容通過參數(shù)‘HttpEntity<?>requestEntity'描述
(3)exchange支持‘含參數(shù)的類型'(即泛型類)作為返回類型,該特性通過‘ParameterizedTypeReference<T>responseType'描述。
RestTemplate所有的GET,POST等等方法,最終調(diào)用的都是execute方法。excute方法的內(nèi)部實現(xiàn)是將String格式的URI轉(zhuǎn)成了java.net.URI,之后調(diào)用了doExecute方法,doExecute方法的實現(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)建連接、處理請求和應(yīng)答,關(guān)閉連接等。
多數(shù)人看到這里,估計都會覺得封裝一個RestClient不過如此吧?
3、簡單調(diào)用
以一個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測試用) **/ @Component public class GoodsServiceClient { //服務(wù)消費者調(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)用,反序列化實體轉(zhuǎn)換這些RestTemplate內(nèi)部封裝搞定。
二、問題匯總
1、no suitable HttpMessageConverter found for request type異常
這個問題通常會出現(xiàn)在postForObject中傳入對象進行調(diào)用的時候。
分析RestTemplate源碼,在HttpEntityRequestCallback類的doWithRequest方法中,如果 messageConverters (這個字段后面會繼續(xù)提及)列表字段循環(huán)處理的過程中沒有滿足return跳出的邏輯(也就是沒有匹配的HttpMessageConverter),則拋出上述異常:
HttpEntityRequestCallback.doWithRequest
@Override @SuppressWarnings("unchecked") public void doWithRequest(ClientHttpRequest httpRequest) throws IOException { super.doWithRequest(httpRequest); Object requestBody = this.requestEntity.getBody(); if (requestBody == null) { HttpHeaders httpHeaders = httpRequest.getHeaders(); HttpHeaders requestHeaders = this.requestEntity.getHeaders(); if (!requestHeaders.isEmpty()) { for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) { httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue())); } } if (httpHeaders.getContentLength() < 0) { httpHeaders.setContentLength(0L); } } else { Class<?> requestBodyClass = requestBody.getClass(); Type requestBodyType = (this.requestEntity instanceof RequestEntity ? ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass); HttpHeaders httpHeaders = httpRequest.getHeaders(); HttpHeaders requestHeaders = this.requestEntity.getHeaders(); MediaType requestContentType = requestHeaders.getContentType(); for (HttpMessageConverter<?> messageConverter : getMessageConverters()) { if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<Object> genericConverter = (GenericHttpMessageConverter<Object>) messageConverter; if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) { if (!requestHeaders.isEmpty()) { for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) { httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue())); } } if (logger.isDebugEnabled()) { if (requestContentType != null) { logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + "\" using [" + messageConverter + "]"); } else { logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]"); } } genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest); return; } } else if (messageConverter.canWrite(requestBodyClass, requestContentType)) { if (!requestHeaders.isEmpty()) { for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) { httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue())); } } if (logger.isDebugEnabled()) { if (requestContentType != null) { logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + "\" using [" + messageConverter + "]"); } else { logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]"); } } ((HttpMessageConverter<Object>) messageConverter).write( requestBody, requestContentType, httpRequest); return; } } String message = "Could not write request: no suitable HttpMessageConverter found for request type [" + requestBodyClass.getName() + "]"; if (requestContentType != null) { message += " and content type [" + requestContentType + "]"; } throw new RestClientException(message); } }
最簡單的解決方案是,可以通過包裝http請求頭,并將請求對象序列化成字符串的形式傳參,參考示例代碼如下:
postForObject
/* * Post請求調(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; }
如果我們還想直接返回對象,直接反序列化返回的字符串即可:
postForObject
/* * Post請求調(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ā)起請求發(fā)生異常一樣,處理應(yīng)答的時候也會有問題。
StackOverflow上有人問過相同的問題,根本原因是HTTP消息轉(zhuǎn)換器HttpMessageConverter缺少 MIME Type ,也就是說HTTP在把輸出結(jié)果傳送到客戶端的時候,客戶端必須啟動適當(dāng)?shù)膽?yīng)用程序來處理這個輸出文檔,這可以通過多種MIME(多功能網(wǎng)際郵件擴充協(xié)議)Type來完成。
對于服務(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 ,這是一個比較通用的轉(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)用一個外部天氣預(yù)報接口,如果使用RestTemplate的默認(rèn)配置,直接返回一個字符串應(yīng)答是沒有問題的:
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源代碼,一路跟蹤下去會發(fā)現(xiàn) HttpMessageConverterExtractor 類的extractData方法有個解析應(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)該加進去,貼一下我認(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()); // 啟動的時候要注意,由于我們在服務(wù)中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加會出現(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; } }
看到上面的代碼,再對比一下RestTemplate內(nèi)部實現(xiàn),就知道我參考了RestTemplate的源碼,有潔癖的人可能會說這一坨代碼有點啰嗦,上面那一堆static final的變量和messageConverters填充數(shù)據(jù)方法,暴露了RestTemplate的實現(xiàn),如果RestTemplate修改了,這里也要改,非常不友好,而且看上去一點也不OO。
經(jīng)過分析,RestTemplateBuilder.build()構(gòu)造了RestTemplate對象,只要將內(nèi)部MappingJackson2HttpMessageConverter修改一下支持的MediaType即可,RestTemplate的messageConverters字段雖然是private final的,我們依然可以通過反射修改之,改進后的代碼如下:
RestTemplateConfig
package com.power.demo.restclient.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @Component public class RestTemplateConfig { // 啟動的時候要注意,由于我們在服務(wù)中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加可能會出現(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 { //通過反射設(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; } }
除了一個messageConverters字段,看上去我們不再關(guān)心RestTemplate那些外部依賴包和內(nèi)部構(gòu)造過程,果然干凈簡潔好維護了很多。
3、亂碼問題
這個也是一個非常經(jīng)典的問題。解決方案非常簡單,找到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比較特殊,有人反饋過發(fā)生亂碼問題由它默認(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); } }
如果在使用過程中發(fā)生亂碼,我們可以通過方法設(shè)置HttpMessageConverter支持的編碼,常用的有UTF-8、GBK等。
4、反序列化異常
這是開發(fā)過程中容易碰到的又一個問題。因為Java的開源框架和工具類非常之多,而且版本更迭頻繁,所以經(jīng)常發(fā)生一些意想不到的坑。
以joda time為例,joda time是流行的java時間和日期框架,但是如果你的接口對外暴露joda time的類型,比如DateTime,那么接口調(diào)用方(同構(gòu)和異構(gòu)系統(tǒng))可能會碰到序列化難題,反序列化時甚至直接拋出如下異常:
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);
我在前廠就碰到過,后來為了調(diào)用方便,改回直接暴露Java的Date類型。
當(dāng)然解決的方案不止這一種,可以使用jackson支持自定義類的序列化和反序列化的方式。在精度要求不是很高的系統(tǒng)里,實現(xià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會將joda time序列化為較為復(fù)雜的形式,不利于閱讀,并且對象較大。 * <p> * JodaTime 序列化的時候可以將datetime序列化為字符串,更容易讀 **/ public class DateTimeSerializer extends JsonSerializer<DateTime> { private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); @Override public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeString(value.toString(dateFormatter)); } }
以及DateTime反序列化:
DatetimeDeserializer
package com.power.demo.util; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.io.IOException; /** * JodaTime 反序列化將字符串轉(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類中對常見調(diào)用問題進行匯總處理,可以參考如下:
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 { // 啟動的時候要注意,由于我們在服務(wù)中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認(rèn)已經(jīng)注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); //注冊model,用于實現(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); //不加會出現(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 { //通過反射設(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)用問題,而且不需要你寫RestTemplate幫助工具類了。
上面列舉的這些常見問題,其實.NET下面也有,有興趣大家可以搜索一下微軟的HttpClient常見使用問題,用過的人都深有體會。更不用提 RestSharp 這個開源類庫,幾年前用的過程中發(fā)現(xiàn)了非常多的Bug,到現(xiàn)在還有一個反序列化數(shù)組的問題困擾著我們,我只好自己造個簡單輪子特殊處理,給我最深刻的經(jīng)驗就是,很多看上去簡單的功能,真的碰到了依然會花掉不少的時間去排查和解決,甚至要翻看源碼。所以,我們寫代碼要認(rèn)識到,越是通用的工具,越需要考慮到特例,可能你需要花80%以上的精力去處理20%的特殊情況,這估計也是滿足常見的二八定律吧。
參考:
https://stackoverflow.com/questions/21854369/no-suitable-httpmessageconverter-found-for-response-type
https://stackoverflow.com/questions/40726145/rest-templatecould-not-extract-response-no-suitable-httpmessageconverter-found
https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter
http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。
標(biāo)題名稱:SpringBoot使用RestTemplate消費REST服務(wù)的幾個問題記錄
網(wǎng)頁鏈接:http://www.rwnh.cn/article32/gpocsc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站改版、關(guān)鍵詞優(yōu)化、網(wǎng)站制作、微信公眾號、網(wǎng)站導(dǎo)航、網(wǎng)站內(nèi)鏈
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)