天天看點

resttemplate 請求重試_RestTemplate實踐(及遇到的問題)

在微服務都是以HTTP接口的形式暴露自身服務的,是以在調用遠端服務時就必須使用HTTP用戶端。我們可以使用JDK原生的URLConnection、Apache的Http Client、Netty的異步HTTP Client, Spring的RestTemplate。但是,用起來最友善、最優雅的還是要屬Feign了。這裡介紹的是RestTemplate。

什麼是RestTemplate?

RestTemplate是Spring提供的用于通路Rest服務的用戶端,

RestTemplate提供了多種便捷通路遠端Http服務的方法,能夠大大提高用戶端的編寫效率。

調用RestTemplate的預設構造函數,RestTemplate對象在底層通過使用java.net包下的實作建立HTTP 請求,

可以通過使用ClientHttpRequestFactory指定不同的HTTP請求方式。

ClientHttpRequestFactory接口主要提供了兩種實作方式

1、一種是SimpleClientHttpRequestFactory,使用J2SE提供的方式(既java.net包提供的方式)建立底層的Http請求連接配接。

2、一種方式是使用HttpComponentsClientHttpRequestFactory方式,底層使用HttpClient通路遠端的Http服務,使用HttpClient可以配置連接配接池和證書等資訊。

RestTemplate的核心之一 Http Client。

目前通過RestTemplate 的源碼可知,RestTemplate 可支援多種 Http Client的http的通路,如下所示:

基于 JDK HttpURLConnection 的 SimpleClientHttpRequestFactory,預設。

基于 Apache HttpComponents Client 的 HttpComponentsClientHttpRequestFactory

基于 OkHttp3的OkHttpClientHttpRequestFactory。

基于 Netty4 的 Netty4ClientHttpRequestFactory。

其中HttpURLConnection 和 HttpClient 為原生的網絡通路類,OkHttp3采用了 OkHttp3的架構,Netty4 采用了Netty架構。

xml配置的方式

請檢視RestTemplate源碼了解細節,知其然知其是以然!

RestTemplate預設是使用SimpleClientHttpRequestFactory,内部是調用jdk的HttpConnection,預設逾時為-1

@Autowired

RestTemplate simpleRestTemplate;

@Autowired

RestTemplate restTemplate;

基于jdk的spring的RestTemplate

text/plain;charset=UTF-8

使用Httpclient連接配接池的方式

text/plain;charset=UTF-8

bean初始化+靜态工具

線程安全的單例(懶漢模式)

基于jdk的spring的RestTemplate

importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.context.annotation.Lazy;importorg.springframework.http.client.SimpleClientHttpRequestFactory;importorg.springframework.http.converter.FormHttpMessageConverter;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.http.converter.StringHttpMessageConverter;importorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter;importorg.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;importorg.springframework.stereotype.Component;importorg.springframework.web.client.DefaultResponseErrorHandler;importorg.springframework.web.client.RestTemplate;importjavax.annotation.PostConstruct;importjava.nio.charset.Charset;importjava.util.ArrayList;importjava.util.List;@Component

@Lazy(false)public classSimpleRestClient {private static final Logger LOGGER = LoggerFactory.getLogger(SimpleRestClient.class);private staticRestTemplate restTemplate;static{

SimpleClientHttpRequestFactory requestFactory= newSimpleClientHttpRequestFactory();

requestFactory.setReadTimeout(5000);

requestFactory.setConnectTimeout(5000);//添加轉換器

List> messageConverters = new ArrayList<>();

messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));

messageConverters.add(newFormHttpMessageConverter());

messageConverters.add(newMappingJackson2XmlHttpMessageConverter());

messageConverters.add(newMappingJackson2HttpMessageConverter());

restTemplate= newRestTemplate(messageConverters);

restTemplate.setRequestFactory(requestFactory);

restTemplate.setErrorHandler(newDefaultResponseErrorHandler());

LOGGER.info("SimpleRestClient初始化完成");

}privateSimpleRestClient() {

}

@PostConstructpublic staticRestTemplate getClient() {returnrestTemplate;

}

}

使用Httpclient連接配接池的方式

importorg.apache.http.Header;importorg.apache.http.client.HttpClient;importorg.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;importorg.apache.http.impl.client.DefaultHttpRequestRetryHandler;importorg.apache.http.impl.client.HttpClientBuilder;importorg.apache.http.impl.client.HttpClients;importorg.apache.http.impl.conn.PoolingHttpClientConnectionManager;importorg.apache.http.message.BasicHeader;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.context.annotation.Lazy;importorg.springframework.http.client.HttpComponentsClientHttpRequestFactory;importorg.springframework.http.converter.FormHttpMessageConverter;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.http.converter.StringHttpMessageConverter;importorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter;importorg.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;importorg.springframework.stereotype.Component;importorg.springframework.web.client.DefaultResponseErrorHandler;importorg.springframework.web.client.RestTemplate;importjavax.annotation.PostConstruct;importjava.nio.charset.Charset;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.TimeUnit;@Component

@Lazy(false)public classRestClient {private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);private staticRestTemplate restTemplate;static{//長連接配接保持30秒

PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);//總連接配接數

pollingConnectionManager.setMaxTotal(1000);//同路由的并發數

pollingConnectionManager.setDefaultMaxPerRoute(1000);

HttpClientBuilder httpClientBuilder=HttpClients.custom();

httpClientBuilder.setConnectionManager(pollingConnectionManager);//重試次數,預設是3次,沒有開啟

httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(2, true));//保持長連接配接配置,需要在頭添加Keep-Alive

httpClientBuilder.setKeepAliveStrategy(newDefaultConnectionKeepAliveStrategy());//RequestConfig.Builder builder = RequestConfig.custom();//builder.setConnectionRequestTimeout(200);//builder.setConnectTimeout(5000);//builder.setSocketTimeout(5000);//

//RequestConfig requestConfig = builder.build();//httpClientBuilder.setDefaultRequestConfig(requestConfig);

List headers = new ArrayList<>();

headers.add(new BasicHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));

headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));

headers.add(new BasicHeader("Accept-Language", "zh-CN"));

headers.add(new BasicHeader("Connection", "Keep-Alive"));

httpClientBuilder.setDefaultHeaders(headers);

HttpClient httpClient=httpClientBuilder.build();//httpClient連接配接配置,底層是配置RequestConfig

HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = newHttpComponentsClientHttpRequestFactory(httpClient);// 連接配接逾時

clientHttpRequestFactory.setConnectTimeout(5000);

// 資料讀取逾時時間,即SocketTimeout

clientHttpRequestFactory.setReadTimeout(5000);

// 連接配接不夠用的等待時間,不宜過長,必須設定,比如連接配接不夠用時,時間過長将是災難性的

clientHttpRequestFactory.setConnectionRequestTimeout(200);//緩沖請求資料,預設值是true。通過POST或者PUT大量發送資料時,建議将此屬性更改為false,以免耗盡記憶體。//clientHttpRequestFactory.setBufferRequestBody(false);//添加内容轉換器

List> messageConverters = new ArrayList<>();

messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));

messageConverters.add(newFormHttpMessageConverter());

messageConverters.add(newMappingJackson2XmlHttpMessageConverter());

messageConverters.add(newMappingJackson2HttpMessageConverter());

restTemplate= newRestTemplate(messageConverters);

restTemplate.setRequestFactory(clientHttpRequestFactory);

restTemplate.setErrorHandler(newDefaultResponseErrorHandler());

LOGGER.info("RestClient初始化完成");

}privateRestClient() {

}

@PostConstructpublic staticRestTemplate getClient() {returnrestTemplate;

}

}

@Configurationpublic classRestConfig {

@BeanpublicRestTemplate restTemplate(){

RestTemplate restTemplate= newRestTemplate();returnrestTemplate;

}

@Bean("urlConnection")publicRestTemplate urlConnectionRestTemplate(){

RestTemplate restTemplate= new RestTemplate(newSimpleClientHttpRequestFactory());returnrestTemplate;

}

@Bean("httpClient")publicRestTemplate httpClientRestTemplate(){

RestTemplate restTemplate= new RestTemplate(newHttpComponentsClientHttpRequestFactory());returnrestTemplate;

}

@Bean("oKHttp3")publicRestTemplate OKHttp3RestTemplate(){

RestTemplate restTemplate= new RestTemplate(newOkHttp3ClientHttpRequestFactory());returnrestTemplate;

}

}

ErrorHolder

自定義的一個異常結果包裝類

importorg.springframework.http.HttpHeaders;importorg.springframework.http.HttpStatus;importorg.springframework.web.client.HttpClientErrorException;importorg.springframework.web.client.HttpServerErrorException;

public classErrorHolder {privateHttpStatus statusCode;privateString statusText;privateString responseBody;privateHttpHeaders responseHeaders;publicErrorHolder(HttpStatus statusCode, String statusText, String responseBody) {this.statusCode =statusCode;this.statusText =statusText;this.responseBody =responseBody;

}publicErrorHolder(String statusText) {this.statusText =statusText;

}publicHttpStatus getStatusCode() {returnstatusCode;

}public voidsetStatusCode(HttpStatus statusCode) {this.statusCode =statusCode;

}publicString getStatusText() {returnstatusText;

}public voidsetStatusText(String statusText) {this.statusText =statusText;

}publicString getResponseBody() {returnresponseBody;

}public voidsetResponseBody(String responseBody) {this.responseBody =responseBody;

}publicHttpHeaders getResponseHeaders() {returnresponseHeaders;

}public voidsetResponseHeaders(HttpHeaders responseHeaders) {this.responseHeaders =responseHeaders;

}public staticErrorHolder build(Exception exception) {if (exception instanceofHttpServerErrorException) {

HttpServerErrorException e=(HttpServerErrorException) exception;return newErrorHolder(e.getStatusCode(), e.getStatusText(), e.getResponseBodyAsString());

}if (exception instanceofHttpClientErrorException) {

HttpClientErrorException e=(HttpClientErrorException) exception;return newErrorHolder(e.getStatusCode(), e.getStatusText(), e.getResponseBodyAsString());

}return newErrorHolder(exception.getMessage());

}

}

使用樣例

api裡面可以做自動的參數比對:

如:http://you domainn name/test?empNo={empNo},則下面方法的最後一個參數為資料比對參數,會自動根據key進行查找,然後替換

API沒有聲明異常,注意進行異常處理

更多使用文法請檢視API文檔

ResponseEntity> result = RestClient.getClient().exchange(DIVIDE_PLATE_API, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() {}, map("empNo", empNo));

List list =result.getBody();

ResponseEntity result = RestClient.getClient().exchange(DIVIDE_PLATE_API, HttpMethod.GET, HttpEntity.EMPTY, KyArea.class, map("empNo", empNo));

KyArea kyArea= result.getBody();

RestTemplate處理請求狀态碼為非200的傳回資料

預設的 RestTemplate 有個機制是請求狀态碼非200 就抛出異常,會中斷接下來的操作。如果不想中斷對結果資料得解析,可以通過覆寫預設的 ResponseErrorHandler ,見下面的示例,示例中的方法中基本都是空方法,隻要對hasError修改下,讓他一直傳回true,即是不檢查狀态碼及抛異常了。

@Bean("sslRestTemplate")public RestTemplate getRestTemplate() throwsException {

RestTemplate sslRestTemplate= new RestTemplate(newHttpsClientRequestFactory());

ResponseErrorHandler responseErrorHandler= newResponseErrorHandler() {

@Overridepublic boolean hasError(ClientHttpResponse clientHttpResponse) throwsIOException {return true;

}

@Overridepublic void handleError(ClientHttpResponse clientHttpResponse) throwsIOException {

}

};

sslRestTemplate.setErrorHandler(responseErrorHandler);returnsslRestTemplate;

}

或者,修改resttemplate的源碼,把對應的源碼檔案拷貝到自己的項目中,但不推薦。

resttemplate 請求重試_RestTemplate實踐(及遇到的問題)

RestTempate的通路的逾時設定

例如,我用的是Httpclient的連接配接池,RestTemplate的逾時設定依賴HttpClient的内部的三個逾時時間設定。

HttpClient内部有三個逾時時間設定:連接配接池擷取可用連接配接逾時,連接配接逾時,讀取資料逾時:

1.setConnectionRequestTimeout從連接配接池中擷取可用連接配接逾時:設定從connect Manager擷取Connection 逾時時間,機關毫秒。

HttpClient中的要用連接配接時嘗試從連接配接池中擷取,若是在等待了一定的時間後還沒有擷取到可用連接配接(比如連接配接池中沒有空閑連接配接了)則會抛出擷取連接配接逾時異常。

2.連接配接目标逾時connectionTimeout,機關毫秒。

指的是連接配接目标url的連接配接逾時時間,即客服端發送請求到與目标url建立起連接配接的最大時間。如果在該時間範圍内還沒有建立起連接配接,則就抛出connectionTimeOut異常。

如測試的時候,将url改為一個不存在的url:“

resttemplate 請求重試_RestTemplate實踐(及遇到的問題)

http://test.com” ,逾時時間3000ms過後,系統報出異常:    org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms 

3.等待響應逾時(讀取資料逾時)socketTimeout ,機關毫秒。

連接配接上一個url後,擷取response的傳回等待時間 ,即在與目标url建立連接配接後,等待放回response的最大時間,在規定時間内沒有傳回響應的話就抛出SocketTimeout。

測試時,将socketTimeout 設定很短,會報等待響應逾時。

我遇到的問題,restTemplate請求到一個高可用的服務是,傳回的逾時時間是設定值的2倍,是因為負載均衡器傳回的重定向,導緻httpClient底層認為沒有逾時,又請求一次,如果負載均衡器下有兩個節點,就耗費connectionTimeout的雙倍時間。

resttemplate 請求重試_RestTemplate實踐(及遇到的問題)