亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > Java > 正文

Spring Boot使用RestTemplate消費REST服務的幾個問題記錄

2024-07-14 08:41:08
字體:
來源:轉載
供稿:網友

我們可以通過Spring Boot快速開發REST接口,同時也可能需要在實現接口的過程中,通過Spring Boot調用內外部REST接口完成業務邏輯。

在Spring Boot中,調用REST Api常見的一般主要有兩種方式,通過自帶的RestTemplate或者自己開發http客戶端工具實現服務調用。

RestTemplate基本功能非常強大,不過某些特殊場景,我們可能還是更習慣用自己封裝的工具類,比如上傳文件至分布式文件系統、處理帶證書的https請求等。

本文以RestTemplate來舉例,記錄幾個使用RestTemplate調用接口過程中發現的問題和解決方案。

一、RestTemplate簡介

1、什么是RestTemplate

我們自己封裝的HttpClient,通常都會有一些模板代碼,比如建立連接,構造請求頭和請求體,然后根據響應,解析響應信息,最后關閉連接。

RestTemplate是Spring中對HttpClient的再次封裝,簡化了發起HTTP請求以及處理響應的過程,抽象層級更高,減少消費者的模板代碼,使冗余代碼更少。

其實仔細想想Spring Boot下的很多XXXTemplate類,它們也提供各種模板方法,只不過抽象的層次更高,隱藏了更多細節而已。

順便提一下,Spring Cloud有一個聲明式服務調用Feign,是基于Netflix Feign實現的,整合了Spring Cloud Ribbon與 Spring Cloud Hystrix,并且實現了聲明式的Web服務客戶端定義方式。

本質上Feign是在RestTemplate的基礎上對其再次封裝,由它來幫助我們定義和實現依賴服務接口的定義。

2、RestTemplate常見方法

常見的REST服務有很多種請求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate實現了最常見的方式,用的最多的就是Get和Post了,調用API可參考源碼,這里列舉幾個方法定義(GET、POST、DELETE):

methods

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables)public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)public void delete(String url, Object... uriVariables)public void delete(URI url)

同時要注意兩個較為“靈活”的方法 exchange 和 execute 。

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

(1)允許調用者指定HTTP請求的方法(GET,POST,DELETE等)

(2)可以在請求中增加body以及頭信息,其內容通過參數‘HttpEntity<?>requestEntity'描述

(3)exchange支持‘含參數的類型'(即泛型類)作為返回類型,該特性通過‘ParameterizedTypeReference<T>responseType'描述。

RestTemplate所有的GET,POST等等方法,最終調用的都是execute方法。excute方法的內部實現是將String格式的URI轉成了java.net.URI,之后調用了doExecute方法,doExecute方法的實現如下:

doExecute

 /**  * Execute the given method on the provided URI.  * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};  * the response with the {@link ResponseExtractor}.  * @param url the fully-expanded URL to connect to  * @param method the HTTP method to execute (GET, POST, etc.)  * @param requestCallback object that prepares the request (can be {@code null})  * @param responseExtractor object that extracts the return value from the response (can be {@code null})  * @return an arbitrary object, as returned by the {@link ResponseExtractor}  */ @Nullable protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,   @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {  Assert.notNull(url, "'url' must not be null");  Assert.notNull(method, "'method' must not be null");  ClientHttpResponse response = null;  try {   ClientHttpRequest request = createRequest(url, method);   if (requestCallback != null) {    requestCallback.doWithRequest(request);   }   response = request.execute();   handleResponse(url, method, response);   if (responseExtractor != null) {    return responseExtractor.extractData(response);   }   else {    return null;   }  }  catch (IOException ex) {   String resource = url.toString();   String query = url.getRawQuery();   resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);   throw new ResourceAccessException("I/O error on " + method.name() +     " request for /"" + resource + "/": " + ex.getMessage(), ex);  }  finally {   if (response != null) {    response.close();   }  } }

doExecute方法封裝了模板方法,比如創建連接、處理請求和應答,關閉連接等。

多數人看到這里,估計都會覺得封裝一個RestClient不過如此吧?

3、簡單調用

以一個POST調用為例:

GoodsServiceClient

package com.power.demo.restclient;import com.power.demo.common.AppConst;import com.power.demo.restclient.clientrequest.ClientGetGoodsByGoodsIdRequest;import com.power.demo.restclient.clientresponse.ClientGetGoodsByGoodsIdResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;/** * 商品REST接口客戶端 (demo測試用) **/@Componentpublic class GoodsServiceClient { //服務消費者調用的接口URL 形如:http://localhost:9090 @Value("${spring.power.serviceurl}") private String _serviceUrl; @Autowired private RestTemplate restTemplate; public ClientGetGoodsByGoodsIdResponse getGoodsByGoodsId(ClientGetGoodsByGoodsIdRequest request) {  String svcUrl = getGoodsSvcUrl() + "/getinfobyid";  ClientGetGoodsByGoodsIdResponse response = null;  try {   response = restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.class);  } catch (Exception e) {   e.printStackTrace();   response = new ClientGetGoodsByGoodsIdResponse();   response.setCode(AppConst.FAIL);   response.setMessage(e.toString());  }  return response; } private String getGoodsSvcUrl() {  String url = "";  if (_serviceUrl == null) {   _serviceUrl = "";  }  if (_serviceUrl.length() == 0) {   return url;  }  if (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/") {   url = String.format("%sapi/v1/goods", _serviceUrl);  } else {   url = String.format("%s/api/v1/goods", _serviceUrl);  }  return url; }}

demo里直接RestTemplate.postForObject方法調用,反序列化實體轉換這些RestTemplate內部封裝搞定。

二、問題匯總

1、no suitable HttpMessageConverter found for request type異常

這個問題通常會出現在postForObject中傳入對象進行調用的時候。

分析RestTemplate源碼,在HttpEntityRequestCallback類的doWithRequest方法中,如果 messageConverters (這個字段后面會繼續提及)列表字段循環處理的過程中沒有滿足return跳出的邏輯(也就是沒有匹配的HttpMessageConverter),則拋出上述異常:

HttpEntityRequestCallback.doWithRequest

  @Override  @SuppressWarnings("unchecked")  public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {   super.doWithRequest(httpRequest);   Object requestBody = this.requestEntity.getBody();   if (requestBody == null) {    HttpHeaders httpHeaders = httpRequest.getHeaders();    HttpHeaders requestHeaders = this.requestEntity.getHeaders();    if (!requestHeaders.isEmpty()) {     for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {      httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));     }    }    if (httpHeaders.getContentLength() < 0) {     httpHeaders.setContentLength(0L);    }   }   else {    Class<?> requestBodyClass = requestBody.getClass();    Type requestBodyType = (this.requestEntity instanceof RequestEntity ?      ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);    HttpHeaders httpHeaders = httpRequest.getHeaders();    HttpHeaders requestHeaders = this.requestEntity.getHeaders();    MediaType requestContentType = requestHeaders.getContentType();    for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {     if (messageConverter instanceof GenericHttpMessageConverter) {      GenericHttpMessageConverter<Object> genericConverter =        (GenericHttpMessageConverter<Object>) messageConverter;      if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {       if (!requestHeaders.isEmpty()) {        for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {         httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));        }       }       if (logger.isDebugEnabled()) {        if (requestContentType != null) {         logger.debug("Writing [" + requestBody + "] as /"" + requestContentType +           "/" using [" + messageConverter + "]");        }        else {         logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");        }       }       genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);       return;      }     }     else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {      if (!requestHeaders.isEmpty()) {       for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {        httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));       }      }      if (logger.isDebugEnabled()) {       if (requestContentType != null) {        logger.debug("Writing [" + requestBody + "] as /"" + requestContentType +          "/" using [" + messageConverter + "]");       }       else {        logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");       }      }      ((HttpMessageConverter<Object>) messageConverter).write(        requestBody, requestContentType, httpRequest);      return;     }    }    String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +      requestBodyClass.getName() + "]";    if (requestContentType != null) {     message += " and content type [" + requestContentType + "]";    }    throw new RestClientException(message);   }  }

最簡單的解決方案是,可以通過包裝http請求頭,并將請求對象序列化成字符串的形式傳參,參考示例代碼如下:

postForObject

 /*  * Post請求調用  * */ public static String postForObject(RestTemplate restTemplate, String url, Object params) {  HttpHeaders headers = new HttpHeaders();  MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");  headers.setContentType(type);  headers.add("Accept", MediaType.APPLICATION_JSON.toString());  String json = SerializeUtil.Serialize(params);  HttpEntity<String> formEntity = new HttpEntity<String>(json, headers);  String result = restTemplate.postForObject(url, formEntity, String.class);  return result; }

如果我們還想直接返回對象,直接反序列化返回的字符串即可:

postForObject

 /*  * Post請求調用  * */ public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) {  T response = null;  String respStr = postForObject(restTemplate, url, params);  response = SerializeUtil.DeSerialize(respStr, clazz);  return response; }

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

2、no suitable HttpMessageConverter found for response type異常

和發起請求發生異常一樣,處理應答的時候也會有問題。

StackOverflow上有人問過相同的問題,根本原因是HTTP消息轉換器HttpMessageConverter缺少 MIME Type ,也就是說HTTP在把輸出結果傳送到客戶端的時候,客戶端必須啟動適當的應用程序來處理這個輸出文檔,這可以通過多種MIME(多功能網際郵件擴充協議)Type來完成。

對于服務端應答,很多HttpMessageConverter默認支持的媒體類型(MIMEType)都不同。StringHttpMessageConverter默認支持的則是MediaType.TEXT_PLAIN,SourceHttpMessageConverter默認支持的則是MediaType.TEXT_XML,FormHttpMessageConverter默認支持的是MediaType.APPLICATION_FORM_URLENCODED和MediaType.MULTIPART_FORM_DATA,在REST服務中,我們用到的最多的還是 MappingJackson2HttpMessageConverter ,這是一個比較通用的轉化器(繼承自GenericHttpMessageConverter接口),根據分析,它默認支持的MIMEType為MediaType.APPLICATION_JSON:

MappingJackson2HttpMessageConverter

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

但是有些應用接口默認的應答MIMEType不是application/json,比如我們調用一個外部天氣預報接口,如果使用RestTemplate的默認配置,直接返回一個字符串應答是沒有問題的:

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

但是,如果我們想直接返回一個實體對象:

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

則直接報異常:

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

and content type [application/octet-stream]

很多人碰到過這個問題,首次碰到估計大多都比較懵吧,很多接口都是json或者xml或者plain text格式返回的,什么是application/octet-stream?

查看RestTemplate源代碼,一路跟蹤下去會發現 HttpMessageConverterExtractor 類的extractData方法有個解析應答及反序列化邏輯,如果不成功,拋出的異常信息和上述一致:

HttpMessageConverterExtractor.extractData

 @Override @SuppressWarnings({"unchecked", "rawtypes", "resource"}) public T extractData(ClientHttpResponse response) throws IOException {  MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);  if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {   return null;  }  MediaType contentType = getContentType(responseWrapper);  try {   for (HttpMessageConverter<?> messageConverter : this.messageConverters) {    if (messageConverter instanceof GenericHttpMessageConverter) {     GenericHttpMessageConverter<?> genericMessageConverter =       (GenericHttpMessageConverter<?>) messageConverter;     if (genericMessageConverter.canRead(this.responseType, null, contentType)) {      if (logger.isDebugEnabled()) {       logger.debug("Reading [" + this.responseType + "] as /"" +         contentType + "/" using [" + messageConverter + "]");      }      return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);     }    }    if (this.responseClass != null) {     if (messageConverter.canRead(this.responseClass, contentType)) {      if (logger.isDebugEnabled()) {       logger.debug("Reading [" + this.responseClass.getName() + "] as /"" +         contentType + "/" using [" + messageConverter + "]");      }      return (T) messageConverter.read((Class) this.responseClass, responseWrapper);     }    }   }  }  catch (IOException | HttpMessageNotReadableException ex) {   throw new RestClientException("Error while extracting response for type [" +     this.responseType + "] and content type [" + contentType + "]", ex);  }  throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " +    "for response type [" + this.responseType + "] and content type [" + contentType + "]"); }

StackOverflow上的解決的示例代碼可以接受,但是并不準確,常見的MIMEType都應該加進去,貼一下我認為正確的代碼:

RestTemplateConfig

package com.power.demo.restclient.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.common.collect.Lists;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.context.annotation.Bean;import org.springframework.http.MediaType;import org.springframework.http.converter.*;import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;import org.springframework.http.converter.json.GsonHttpMessageConverter;import org.springframework.http.converter.json.JsonbHttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;import org.springframework.http.converter.xml.SourceHttpMessageConverter;import org.springframework.stereotype.Component;import org.springframework.util.ClassUtils;import org.springframework.web.client.RestTemplate;import java.util.Arrays;import java.util.List;@Componentpublic class RestTemplateConfig { private static final boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", RestTemplate   .class.getClassLoader()); private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader()); private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()); private static final boolean jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.class.getClassLoader()); private static final boolean jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader()); private static final boolean jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.class.getClassLoader()); // 啟動的時候要注意,由于我們在服務中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認已經注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() {  RestTemplate restTemplate = builder.build();  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();  converter.setObjectMapper(objectMapper);  //不加會出現異常  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]  MediaType[] mediaTypes = new MediaType[]{    MediaType.APPLICATION_JSON,    MediaType.APPLICATION_OCTET_STREAM,    MediaType.APPLICATION_JSON_UTF8,    MediaType.TEXT_HTML,    MediaType.TEXT_PLAIN,    MediaType.TEXT_XML,    MediaType.APPLICATION_STREAM_JSON,    MediaType.APPLICATION_ATOM_XML,    MediaType.APPLICATION_FORM_URLENCODED,    MediaType.APPLICATION_PDF,  };  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));  //messageConverters.add(converter);  if (jackson2Present) {   messageConverters.add(converter);  } else if (gsonPresent) {   messageConverters.add(new GsonHttpMessageConverter());  } else if (jsonbPresent) {   messageConverters.add(new JsonbHttpMessageConverter());  }  messageConverters.add(new FormHttpMessageConverter());  messageConverters.add(new ByteArrayHttpMessageConverter());  messageConverters.add(new StringHttpMessageConverter());  messageConverters.add(new ResourceHttpMessageConverter(false));  messageConverters.add(new SourceHttpMessageConverter());  messageConverters.add(new AllEncompassingFormHttpMessageConverter());  if (romePresent) {   messageConverters.add(new AtomFeedHttpMessageConverter());   messageConverters.add(new RssChannelHttpMessageConverter());  }  if (jackson2XmlPresent) {   messageConverters.add(new MappingJackson2XmlHttpMessageConverter());  } else if (jaxb2Present) {   messageConverters.add(new Jaxb2RootElementHttpMessageConverter());  }  if (jackson2SmilePresent) {   messageConverters.add(new MappingJackson2SmileHttpMessageConverter());  }  if (jackson2CborPresent) {   messageConverters.add(new MappingJackson2CborHttpMessageConverter());  }  restTemplate.setMessageConverters(messageConverters);  return restTemplate; }}

看到上面的代碼,再對比一下RestTemplate內部實現,就知道我參考了RestTemplate的源碼,有潔癖的人可能會說這一坨代碼有點啰嗦,上面那一堆static final的變量和messageConverters填充數據方法,暴露了RestTemplate的實現,如果RestTemplate修改了,這里也要改,非常不友好,而且看上去一點也不OO。

經過分析,RestTemplateBuilder.build()構造了RestTemplate對象,只要將內部MappingJackson2HttpMessageConverter修改一下支持的MediaType即可,RestTemplate的messageConverters字段雖然是private final的,我們依然可以通過反射修改之,改進后的代碼如下:

RestTemplateConfig

package com.power.demo.restclient.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.google.common.collect.Lists;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.context.annotation.Bean;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;import java.lang.reflect.Field;import java.util.Arrays;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;@Componentpublic class RestTemplateConfig { // 啟動的時候要注意,由于我們在服務中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認已經注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() {  RestTemplate restTemplate = builder.build();  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();  converter.setObjectMapper(objectMapper);  //不加可能會出現異常  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]  MediaType[] mediaTypes = new MediaType[]{    MediaType.APPLICATION_JSON,    MediaType.APPLICATION_OCTET_STREAM,    MediaType.TEXT_HTML,    MediaType.TEXT_PLAIN,    MediaType.TEXT_XML,    MediaType.APPLICATION_STREAM_JSON,    MediaType.APPLICATION_ATOM_XML,    MediaType.APPLICATION_FORM_URLENCODED,    MediaType.APPLICATION_JSON_UTF8,    MediaType.APPLICATION_PDF,  };  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));  try {   //通過反射設置MessageConverters   Field field = restTemplate.getClass().getDeclaredField("messageConverters");   field.setAccessible(true);   List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate);   Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream()     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class       .getName()))     .findFirst();   if (opConverter.isPresent() == false) {    return restTemplate;   }   messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter   //添加原有的剩余的HttpMessageConverter   List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream()     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class       .getName()) == false)     .collect(Collectors.toList());   messageConverters.addAll(leftConverters);   System.out.println(String.format("【HttpMessageConverter】原有數量:%s,重新構造后數量:%s"     , orgConverterList.size(), messageConverters.size()));  } catch (Exception e) {   e.printStackTrace();  }  restTemplate.setMessageConverters(messageConverters);  return restTemplate; }}

除了一個messageConverters字段,看上去我們不再關心RestTemplate那些外部依賴包和內部構造過程,果然干凈簡潔好維護了很多。

3、亂碼問題

這個也是一個非常經典的問題。解決方案非常簡單,找到HttpMessageConverter,看看默認支持的Charset。AbstractJackson2HttpMessageConverter是很多HttpMessageConverter的基類,默認編碼為UTF-8:

AbstractJackson2HttpMessageConverter

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;}

而StringHttpMessageConverter比較特殊,有人反饋過發生亂碼問題由它默認支持的編碼 ISO-8859-1 引起:

StringHttpMessageConverter

/** * Implementation of {@link HttpMessageConverter} that can read and write strings. * * <p>By default, this converter supports all media types ({@code }), * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. * * @author Arjen Poutsma * @author Juergen Hoeller * @since 3.0 */public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> { public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; /**  * A default constructor that uses {@code "ISO-8859-1"} as the default charset.  * @see #StringHttpMessageConverter(Charset)  */ public StringHttpMessageConverter() {  this(DEFAULT_CHARSET); }}

如果在使用過程中發生亂碼,我們可以通過方法設置HttpMessageConverter支持的編碼,常用的有UTF-8、GBK等。

4、反序列化異常

這是開發過程中容易碰到的又一個問題。因為Java的開源框架和工具類非常之多,而且版本更迭頻繁,所以經常發生一些意想不到的坑。

以joda time為例,joda time是流行的java時間和日期框架,但是如果你的接口對外暴露joda time的類型,比如DateTime,那么接口調用方(同構和異構系統)可能會碰到序列化難題,反序列化時甚至直接拋出如下異常:

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

我在前廠就碰到過,后來為了調用方便,改回直接暴露Java的Date類型。

當然解決的方案不止這一種,可以使用jackson支持自定義類的序列化和反序列化的方式。在精度要求不是很高的系統里,實現簡單的DateTime自定義序列化:

DateTimeSerializer

package com.power.demo.util;import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JsonSerializer;import com.fasterxml.jackson.databind.SerializerProvider;import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;import java.io.IOException;/** * 在默認情況下,jackson會將joda time序列化為較為復雜的形式,不利于閱讀,并且對象較大。 * <p> * JodaTime 序列化的時候可以將datetime序列化為字符串,更容易讀 **/public class DateTimeSerializer extends JsonSerializer<DateTime> { private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); @Override public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {  jgen.writeString(value.toString(dateFormatter)); }}

以及DateTime反序列化:

DatetimeDeserializer

package com.power.demo.util;import com.fasterxml.jackson.core.JsonParser;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.DeserializationContext;import com.fasterxml.jackson.databind.JsonDeserializer;import com.fasterxml.jackson.databind.JsonNode;import org.joda.time.DateTime;import org.joda.time.format.DateTimeFormat;import org.joda.time.format.DateTimeFormatter;import java.io.IOException;/** * JodaTime 反序列化將字符串轉化為datetime **/public class DatetimeDeserializer extends JsonDeserializer<DateTime> { private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); @Override public DateTime deserialize(JsonParser jp, DeserializationContext context) throws IOException, JsonProcessingException {  JsonNode node = jp.getCodec().readTree(jp);  String s = node.asText();  DateTime parse = DateTime.parse(s, dateFormatter);  return parse; }}

最后可以在RestTemplateConfig類中對常見調用問題進行匯總處理,可以參考如下:

RestTemplateConfig

package com.power.demo.restclient.config;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module.SimpleModule;import com.google.common.collect.Lists;import com.power.demo.util.DateTimeSerializer;import com.power.demo.util.DatetimeDeserializer;import org.joda.time.DateTime;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.web.client.RestTemplateBuilder;import org.springframework.context.annotation.Bean;import org.springframework.http.MediaType;import org.springframework.http.converter.HttpMessageConverter;import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;import java.lang.reflect.Field;import java.util.Arrays;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;@Componentpublic class RestTemplateConfig { // 啟動的時候要注意,由于我們在服務中注入了RestTemplate,所以啟動的時候需要實例化該類的一個實例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder來實例化RestTemplate對象,spring默認已經注入了RestTemplateBuilder實例 @Bean public RestTemplate restTemplate() {  RestTemplate restTemplate = builder.build();  //注冊model,用于實現jackson joda time序列化和反序列化  SimpleModule module = new SimpleModule();  module.addSerializer(DateTime.class, new DateTimeSerializer());  module.addDeserializer(DateTime.class, new DatetimeDeserializer());  objectMapper.registerModule(module);  List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList();  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();  converter.setObjectMapper(objectMapper);  //不加會出現異常  //Could not extract response: no suitable HttpMessageConverter found for response type [class ]  MediaType[] mediaTypes = new MediaType[]{    MediaType.APPLICATION_JSON,    MediaType.APPLICATION_OCTET_STREAM,    MediaType.TEXT_HTML,    MediaType.TEXT_PLAIN,    MediaType.TEXT_XML,    MediaType.APPLICATION_STREAM_JSON,    MediaType.APPLICATION_ATOM_XML,    MediaType.APPLICATION_FORM_URLENCODED,    MediaType.APPLICATION_JSON_UTF8,    MediaType.APPLICATION_PDF,  };  converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));  try {   //通過反射設置MessageConverters   Field field = restTemplate.getClass().getDeclaredField("messageConverters");   field.setAccessible(true);   List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate);   Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream()     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class       .getName()))     .findFirst();   if (opConverter.isPresent() == false) {    return restTemplate;   }   messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter   //添加原有的剩余的HttpMessageConverter   List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream()     .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class       .getName()) == false)     .collect(Collectors.toList());   messageConverters.addAll(leftConverters);   System.out.println(String.format("【HttpMessageConverter】原有數量:%s,重新構造后數量:%s"     , orgConverterList.size(), messageConverters.size()));  } catch (Exception e) {   e.printStackTrace();  }  restTemplate.setMessageConverters(messageConverters);  return restTemplate; }}

目前良好地解決了RestTemplate常用調用問題,而且不需要你寫RestTemplate幫助工具類了。

上面列舉的這些常見問題,其實.NET下面也有,有興趣大家可以搜索一下微軟的HttpClient常見使用問題,用過的人都深有體會。更不用提 RestSharp 這個開源類庫,幾年前用的過程中發現了非常多的Bug,到現在還有一個反序列化數組的問題困擾著我們,我只好自己造個簡單輪子特殊處理,給我最深刻的經驗就是,很多看上去簡單的功能,真的碰到了依然會花掉不少的時間去排查和解決,甚至要翻看源碼。所以,我們寫代碼要認識到,越是通用的工具,越需要考慮到特例,可能你需要花80%以上的精力去處理20%的特殊情況,這估計也是滿足常見的二八定律吧。

參考:

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

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

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

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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JAVA教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
亚洲韩国青草视频| 日韩av影片在线观看| 欧美午夜宅男影院在线观看| 在线视频国产日韩| 午夜精品久久久久久久男人的天堂| 成人精品一区二区三区电影黑人| 日韩免费视频在线观看| 日韩欧美国产一区二区| 色妞色视频一区二区三区四区| 国产成人精品国内自产拍免费看| 中文字幕亚洲二区| 日av在线播放中文不卡| 久久久久久久久久久免费精品| 秋霞av国产精品一区| 欧美在线一区二区三区四| 亚洲一区二区三区xxx视频| 国产国语刺激对白av不卡| 日韩亚洲欧美中文高清在线| 国产精品爽黄69| 国产成人精品视频在线| 热久久视久久精品18亚洲精品| 亚洲精品欧美日韩专区| 国产精品成人观看视频国产奇米| 久久精品成人一区二区三区| 日韩免费在线播放| 欧美极品在线视频| 亚洲精品欧美日韩| 国产精品亚洲аv天堂网| 成人精品久久久| 91精品视频观看| 亚洲精品国产精品久久清纯直播| 色先锋资源久久综合5566| 91av视频在线| 成人网在线观看| 日韩成人激情影院| 国产精品xxx视频| 欧美理论片在线观看| 亚洲直播在线一区| 国产91久久婷婷一区二区| 亚洲free嫩bbb| 国产精品久久久久久久久久久久| 国产精品久久久久久中文字| 国产一区玩具在线观看| 狠狠躁天天躁日日躁欧美| 亚洲精品动漫100p| 亚洲成人av片| 欧美成人全部免费| 一区二区欧美日韩视频| 成人免费观看a| 成人免费观看49www在线观看| 亚洲精品国产品国语在线| 国产91精品黑色丝袜高跟鞋| 欧美性猛交xxxx免费看久久久| 久久久久在线观看| 激情久久av一区av二区av三区| 欧美大胆a视频| 中文字幕国产亚洲| 亚洲欧美福利视频| 亚洲开心激情网| 91精品久久久久久久久久另类| 91美女片黄在线观| 亚洲国产高清高潮精品美女| 久久久久久久成人| 欧美成人在线免费| 91精品久久久久久久久久另类| 亚洲国产精品yw在线观看| 亚洲欧美日韩精品久久亚洲区| 亚洲欧美国产精品专区久久| 亚洲性无码av在线| 福利一区视频在线观看| 亚洲一区二区三区乱码aⅴ| 国产精品自在线| 亚洲欧美中文日韩在线| 亚洲黄色免费三级| 亚洲精品美女免费| 国产精品视频在线播放| 2018国产精品视频| 精品偷拍各种wc美女嘘嘘| 亚洲欧美日韩高清| 亚洲欧美日韩一区二区三区在线| 国产精品揄拍一区二区| 亚洲自拍小视频| 亚洲精品一区二区久| 欧美一级片一区| 亚洲国产精品福利| 国产精品激情自拍| 欧美激情奇米色| 日韩av在线资源| 久久九九免费视频| 亚洲国产精品va在线| 成人网在线视频| 在线日韩欧美视频| 成人国产精品免费视频| 91精品国产乱码久久久久久久久| 久久全国免费视频| 日韩亚洲欧美中文在线| 国产成人一区二区三区小说| 中文一区二区视频| 亚洲毛茸茸少妇高潮呻吟| 亚洲欧美一区二区精品久久久| 久久久久久久国产精品| 亚洲色图13p| 欧美高清视频一区二区| 国产精品毛片a∨一区二区三区|国| 国产美女91呻吟求| 日韩在线视频免费观看高清中文| 日韩免费视频在线观看| 久久久久一本一区二区青青蜜月| 国产日韩av在线播放| 国产精品video| 97视频在线观看成人| 尤物yw午夜国产精品视频明星| 欧美日韩一区二区在线| 国产精品一区二区三区成人| 88国产精品欧美一区二区三区| 日本精品性网站在线观看| 日韩一区在线视频| 国产精品嫩草影院久久久| 亚洲人成网站在线播| 日韩av在线网站| 欧美精品久久久久| 日韩在线播放av| 亚洲精品一区在线观看香蕉| 性欧美亚洲xxxx乳在线观看| 国产日韩欧美黄色| 日本精品免费一区二区三区| 国产精品久久久久久搜索| 欧美日韩国产成人在线观看| 久久国产精品久久久久久久久久| 国产久一一精品| 亚洲精品国产美女| 亚洲国产成人在线播放| 久久久久国产精品免费网站| 色噜噜狠狠狠综合曰曰曰88av| 久久精品视频在线观看| 欧美一区三区三区高中清蜜桃| 日韩三级影视基地| 欧美国产视频一区二区| 久久精品在线视频| 国产日本欧美一区二区三区在线| 中文字幕亚洲欧美日韩高清| www.亚洲男人天堂| 久久国产精品久久精品| 久久亚洲精品视频| 性欧美长视频免费观看不卡| 欧美一区二区三区精品电影| 成人免费高清完整版在线观看| 日韩成人高清在线| 国产精品狠色婷| 狠狠久久亚洲欧美专区| 日韩av一区在线观看| 中文字幕在线观看日韩| 日韩中文字幕免费| 97超碰蝌蚪网人人做人人爽| 国产欧美一区二区三区视频| 国产成人jvid在线播放| 日本午夜在线亚洲.国产| 日韩一区二区精品视频| 6080yy精品一区二区三区| 中文字幕不卡在线视频极品| 永久免费毛片在线播放不卡| 欧美尤物巨大精品爽| 国产精品极品在线| 丝袜一区二区三区|