您好,登錄后才能下訂單哦!
怎么在spring boot中對HttpClient進行封裝?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
一、Request retry handler(請求重試處理)
為了使自定義異常機制生效,需要實現HttpRequestRetryHandler接口,代碼如下:
import java.io.IOException; import java.io.InterruptedIOException; import java.net.UnknownHostException; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; import org.apache.http.NoHttpResponseException; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.protocol.HttpContext; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述:HttpClient的重試處理機制 */ @Configuration public class MyhttpRequestRetryHandler { @Value("${httpclient.config.retryTime}")// 此處建議采用@ConfigurationProperties(prefix="httpclient.config")方式,方便復用 private int retryTime; @Bean public HttpRequestRetryHandler httpRequestRetryHandler() { // 請求重試 final int retryTime = this.retryTime; return new HttpRequestRetryHandler() { public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { // Do not retry if over max retry count,如果重試次數超過了retryTime,則不再重試請求 if (executionCount >= retryTime) { return false; } // 服務端斷掉客戶端的連接異常 if (exception instanceof NoHttpResponseException) { return true; } // time out 超時重試 if (exception instanceof InterruptedIOException) { return true; } // Unknown host if (exception instanceof UnknownHostException) { return false; } // Connection refused if (exception instanceof ConnectTimeoutException) { return false; } // SSL handshake exception if (exception instanceof SSLException) { return false; } HttpClientContext clientContext = HttpClientContext.adapt(context); HttpRequest request = clientContext.getRequest(); if (!(request instanceof HttpEntityEnclosingRequest)) { return true; } return false; } }; } }
二、Pooling connection manager(連接池管理)
PoolingHttpClientConnectionManager用來管理客戶端的連接池,并且可以為多個線程的請求提供服務,代碼如下:
import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.LayeredConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyPoolingHttpClientConnectionManager { /** * 連接池最大連接數 */ @Value("${httpclient.config.connMaxTotal}") private int connMaxTotal = 20; /** * */ @Value("${httpclient.config.maxPerRoute}") private int maxPerRoute = 20; /** * 連接存活時間,單位為s */ @Value("${httpclient.config.timeToLive}") private int timeToLive = 60; @Bean public PoolingHttpClientConnectionManager poolingClientConnectionManager(){ PoolingHttpClientConnectionManager poolHttpcConnManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS); // 最大連接數 poolHttpcConnManager.setMaxTotal(this.connMaxTotal); // 路由基數 poolHttpcConnManager.setDefaultMaxPerRoute(this.maxPerRoute); return poolHttpcConnManager; } }
注意:當HttpClient實例不再需要并且即將超出范圍時,重要的是關閉其連接管理器,以確保管理器保持活動的所有連接都被關閉,并釋放由這些連接分配的系統資源
上面PoolingHttpClientConnectionManager類的構造函數如下:
public PoolingHttpClientConnectionManager(final long timeToLive, final TimeUnit tunit) { this(getDefaultRegistry(), null, null ,null, timeToLive, tunit); } private static Registry<ConnectionSocketFactory> getDefaultRegistry() { return RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); }
在PoolingHttpClientConnectionManager的配置中有兩個最大連接數量,分別控制著總的最大連接數量和每個route的最大連接數量。如果沒有顯式設置,默認每個route只允許最多2個connection,總的connection數量不超過20。這個值對于很多并發度高的應用來說是不夠的,必須根據實際的情況設置合適的值,思路和線程池的大小設置方式是類似的,如果所有的連接請求都是到同一個url,那可以把MaxPerRoute的值設置成和MaxTotal一致,這樣就能更高效地復用連接
特別注意:想要復用一個connection就必須要讓它占有的系統資源得到正確釋放,釋放方法如下:
如果是使用outputStream就要保證整個entity都被write out,如果是inputStream,則再最后要記得調用inputStream.close()。或者使用EntityUtils.consume(entity)或EntityUtils.consumeQuietly(entity)來讓entity被完全耗盡(后者不拋異常)來做這一工作。EntityUtils中有個toString方法也很方便的(調用這個方法最后也會自動把inputStream close掉的,但是在實際的測試過程中,會導致連接沒有釋放的現象),不過只有在可以確定收到的entity不是特別大的情況下才能使用。如果沒有讓整個entity被fully consumed,則該連接是不能被復用的,很快就會因為在連接池中取不到可用的連接超時或者阻塞在這里(因為該連接的狀態將會一直是leased的,即正在被使用的狀態)。所以如果想要復用connection,一定一定要記得把entity fully consume掉,只要檢測到stream的eof,是會自動調用ConnectionHolder的releaseConnection方法進行處理的
三、Connection keep alive strategy(保持連接策略)
HTTP規范沒有指定持久連接可能和應該保持存活多久。一些HTTP服務器使用非標準的Keep-Alive標頭來向客戶端通信它們打算在服務器端保持連接的時間段(以秒為單位)。HttpClient可以使用這些信息。如果響應中不存在Keep-Alive頭,HttpClient會假定連接可以無限期地保持活動。然而,一般使用的許多HTTP服務器都配置為在一段不活動狀態之后刪除持久連接,以便節省系統資源,而不會通知客戶端。如果默認策略過于樂觀,則可能需要提供自定義的保持活動策略,代碼如下:
import org.apache.http.HeaderElement; import org.apache.http.HeaderElementIterator; import org.apache.http.HttpResponse; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述:連接保持策略 * @author chhliu */ @Configuration public class MyconnectionKeepAliveStrategy { @Value("${httpclient.config.keepAliveTime}") private int keepAliveTime = 30; @Bean("connectionKeepAliveStrategy") public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() { return new ConnectionKeepAliveStrategy() { public long getKeepAliveDuration(HttpResponse response, HttpContext context) { // Honor 'keep-alive' header HeaderElementIterator it = new BasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); String value = he.getValue(); if (value != null && param.equalsIgnoreCase("timeout")) { try { return Long.parseLong(value) * 1000; } catch (NumberFormatException ignore) { } } } return 30 * 1000; } }; } }
注意:長連接并不使用于所有的情況,尤其現在的系統,大都是部署在多臺服務器上,且具有負載均衡的功能,如果我們在訪問的時候,一直保持長連接,一旦那臺服務器掛了,就會影響客戶端,同時也不能充分的利用服務端的負載均衡的特性,反而短連接更有利一些,這些需要根據具體的需求來定,而不是一言概括。
四、HttpClient proxy configuration(代理配置)
用來配置代理,代碼如下:
import org.apache.http.HttpHost; import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 描述:HttpClient代理 * @author chhliu */ @Configuration public class MyDefaultProxyRoutePlanner { // 代理的host地址 @Value("${httpclient.config.proxyhost}") private String proxyHost; // 代理的端口號 @Value("${httpclient.config.proxyPort}") private int proxyPort = 8080; @Bean public DefaultProxyRoutePlanner defaultProxyRoutePlanner(){ HttpHost proxy = new HttpHost(this.proxyHost, this.proxyPort); return new DefaultProxyRoutePlanner(proxy); } }
HttpClient不僅支持簡單的直連、復雜的路由策略以及代理。HttpRoutePlanner是基于http上下文情況下,客戶端到服務器的路由計算策略,一般沒有代理的話,就不用設置這個東西。這里有一個很關鍵的概念—Route:在HttpClient中,一個Route指 運行環境機器->目標機器host的一條線路,也就是如果目標url的host是同一個,那么它們的route也是一樣的
五、RequestConfig
用來設置請求的各種配置,代碼如下:
import org.apache.http.client.config.RequestConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyRequestConfig { @Value("${httpclient.config.connectTimeout}") private int connectTimeout = 2000; @Value("${httpclient.config.connectRequestTimeout}") private int connectRequestTimeout = 2000; @Value("${httpclient.config.socketTimeout}") private int socketTimeout = 2000; @Bean public RequestConfig config(){ return RequestConfig.custom() .setConnectionRequestTimeout(this.connectRequestTimeout) .setConnectTimeout(this.connectTimeout) .setSocketTimeout(this.socketTimeout) .build(); } }
RequestConfig是對request的一些配置。里面比較重要的有三個超時時間,默認的情況下這三個超時時間都為0(如果不設置request的Config,會在execute的過程中使用HttpClientParamConfig的getRequestConfig中用默認參數進行設置),這也就意味著無限等待,很容易導致所有的請求阻塞在這個地方無限期等待。這三個超時時間為:
a、connectionRequestTimeout—從連接池中取連接的超時時間
這個時間定義的是從ConnectionManager管理的連接池中取出連接的超時時間, 如果連接池中沒有可用的連接,則request會被阻塞,最長等待connectionRequestTimeout的時間,如果還沒有被服務,則拋出ConnectionPoolTimeoutException異常,不繼續等待。
b、connectTimeout—連接超時時間
這個時間定義了通過網絡與服務器建立連接的超時時間,也就是取得了連接池中的某個連接之后到接通目標url的連接等待時間。發生超時,會拋出ConnectionTimeoutException異常。
c、socketTimeout—請求超時時間
這個時間定義了socket讀數據的超時時間,也就是連接到服務器之后到從服務器獲取響應數據需要等待的時間,或者說是連接上一個url之后到獲取response的返回等待時間。發生超時,會拋出SocketTimeoutException異常。
六、實例化HttpClient
通過實現FactoryBean來實例化HttpClient,代碼如下:
import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.config.RequestConfig; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * 描述:HttpClient客戶端封裝 */ @Service("httpClientManagerFactoryBen") public class HttpClientManagerFactoryBen implements FactoryBean<CloseableHttpClient>, InitializingBean, DisposableBean { /** * FactoryBean生成的目標對象 */ private CloseableHttpClient client; @Autowired private ConnectionKeepAliveStrategy connectionKeepAliveStrategy; @Autowired private HttpRequestRetryHandler httpRequestRetryHandler; @Autowired private DefaultProxyRoutePlanner proxyRoutePlanner; @Autowired private PoolingHttpClientConnectionManager poolHttpcConnManager; @Autowired private RequestConfig config; // 銷毀上下文時,銷毀HttpClient實例 @Override public void destroy() throws Exception { /* * 調用httpClient.close()會先shut down connection manager,然后再釋放該HttpClient所占用的所有資源, * 關閉所有在使用或者空閑的connection包括底層socket。由于這里把它所使用的connection manager關閉了, * 所以在下次還要進行http請求的時候,要重新new一個connection manager來build一個HttpClient, * 也就是在需要關閉和新建Client的情況下,connection manager不能是單例的. */ if(null != this.client){ this.client.close(); } } @Override// 初始化實例 public void afterPropertiesSet() throws Exception { /* * 建議此處使用HttpClients.custom的方式來創建HttpClientBuilder,而不要使用HttpClientBuilder.create()方法來創建HttpClientBuilder * 從官方文檔可以得出,HttpClientBuilder是非線程安全的,但是HttpClients確實Immutable的,immutable 對象不僅能夠保證對象的狀態不被改變, * 而且還可以不使用鎖機制就能被其他線程共享 */ this.client = HttpClients.custom().setConnectionManager(poolHttpcConnManager) .setRetryHandler(httpRequestRetryHandler) .setKeepAliveStrategy(connectionKeepAliveStrategy) .setRoutePlanner(proxyRoutePlanner) .setDefaultRequestConfig(config) .build(); } // 返回實例的類型 @Override public CloseableHttpClient getObject() throws Exception { return this.client; } @Override public Class<?> getObjectType() { return (this.client == null ? CloseableHttpClient.class : this.client.getClass()); } // 構建的實例為單例 @Override public boolean isSingleton() { return true; } }
七、增加配置文件
# 代理的host httpclient.config.proxyhost=xxx.xx.xx.xx # 代理端口 httpclient.config.proxyPort=8080 # 連接超時或異常重試次數 httpclient.config.retryTime=3 # 長連接保持時間,單位為s httpclient.config.keepAliveTime=30 # 連接池最大連接數 httpclient.config.connMaxTotal=20 httpclient.config.maxPerRoute=20 # 連接超時時間,單位ms httpclient.config.connectTimeout=2000 # 請求超時時間 httpclient.config.connectRequestTimeout=2000 # sock超時時間 httpclient.config.socketTimeout=2000 # 連接存活時間,單位s httpclient.config.timeToLive=60
八、測試
測試代碼如下:
import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import javax.annotation.Resource; import org.apache.http.Consts; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class HttpClientManagerFactoryBenTest { // 注入HttpClient實例 @Resource(name = "httpClientManagerFactoryBen") private CloseableHttpClient client; @Test public void test() throws ClientProtocolException, IOException, InterruptedException{ ExecutorService service = Executors.newFixedThreadPool(2); for(int i=0; i<10; i++){ service.submit(new Runnable() { @Override public void run() { System.out.println("the current thread is:"+Thread.currentThread().getName()); HttpEntity entity = null; try { HttpGet get = new HttpGet("https://localhost:8080/testjson"); // 通過httpclient的execute提交 請求 ,并用CloseableHttpResponse接受返回信息 CloseableHttpResponse response = client.execute(get); System.out.println("client object:"+client); entity = response.getEntity(); System.out.println("============"+EntityUtils.toString(entity, Consts.UTF_8)+"============="); EntityUtils.consumeQuietly(entity);// 釋放連接 } catch (ClientProtocolException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally{ if(null != entity){// 釋放連接 EntityUtils.consumeQuietly(entity); } } } }); } Thread.sleep(60000); } }
關于怎么在spring boot中對HttpClient進行封裝問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。