JAVA && Spring && SpringBoot2.x — 學習目錄
目錄
- 連接池的設置。
- 獲取連接超時時間、建立連接超時時間、保持連接超時時間的設置。
- 長連接策略的設置。
- 連接逐出策略的設置。
- 重試機制的設置。
- 個性化請求參數的設置。
- 附錄。
序
HttpClient可以用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,并且它支持 HTTP 協議最新的版本和建議。
使用HttpClient發送請求和接收響應的步驟:
- 創建CloseableHttpClient對象;
- 創建請求方法實例,并指定請求URL。例:如果要發送Get請求,創建HttpGet對象;如果要發送POST請求,創建HttpPost對象;
- 如果需要發送參數,則調用setEntity(HttpEntity entity)方法來設置參數;
- 調用HttpGet/HttpPost對象的setHeader(String name,String value)方法設置header信息,或者調用setHeader(Header[] headers)設置一組header參數;
- 調用CloseableHttpClient對象的execute(HttpUriRequest request)發送請求,該方法返回一個CloseableHttpResponse;
- 調用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務器的響應內容。程序可通過該對象獲取服務器的響應內容;調用CloseableHttpResponse的getAllHeaders()、getHeaders(String name)等方法可獲取服務器的響應頭;
- 釋放連接。無論執行方法是否成功,都必須釋放連接
1. 引入Maven依賴
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
1. HttpClient連接池分析
PoolingHttpClientConnectionManager是一個HttpClientConnection的連接池,可以為多線程提供并發請求服務。主要是分配連接,回收連接。同一個遠程請求,會優先使用連接池提供的空閑的長連接。
源碼位置:org.apache.http.impl.conn.PoolingHttpClientConnectionManager
默認構造方法:
/**
* @since 4.4
*/
public PoolingHttpClientConnectionManager(
final HttpClientConnectionOperator httpClientConnectionOperator,
final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
final long timeToLive, final TimeUnit timeUnit) {
super();
this.configData = new ConfigData();
//連接池的默認配置defaultMaxPerRoute默認為2,maxTotal默認為20
this.pool = new CPool(new InternalConnectionFactory(
this.configData, connFactory), 2, 20, timeToLive, timeUnit);
//官方推薦使用這個來檢查永久鏈接的可用性,而不推薦每次請求的時候才去檢查
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}
- maxTotal:連接池的最大連接數。
- defaultMaxPreRount:每個Rount(遠程)請求最大的連接數。
- setValidateAfterInactivity:連接空閑多長時間(單位:毫秒)進行檢查。
顯示的調整連接池參數:
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
使用httpclient必須知道的參數設置及代碼寫法、存在的風險
1.1 MaxTotal和DefaultMaxPerRoute的區別
參數配置:MaxTotal=100,DefaultMaxPerRoute=5
服務器端睡眠2秒。該圖是客戶端的響應信息截圖。
可以看到,只有5筆請求并發的調用遠程服務端,得到響應之后。再次有5筆請求調用服務端。
- MaxtTotal是整個池子的大小;
- DefaultMaxPerRoute是根據連接到的主機對MaxTotal的一個細分;比如:
MaxtTotal=400 DefaultMaxPerRoute=200
而我只連接到http://sishuok.com時,到這個主機的并發最多只有200;而不是400;
而我連接到http://sishuok.com 和 http://qq.com時,到每個主機的并發最多只有200;即加起來是400(但不能超過400);所以起作用的設置是DefaultMaxPerRoute。
2. SpringBoot集成HttpClient
2.1 超時時間設置
httpClient內部有三個超時時間設置:獲取連接的超時時間、建立連接的超時時間、讀取數據超時時間。
//設置網絡配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(2000) //從鏈接池獲取連接的超時時間
.setConnectTimeout(2000) //與服務器連接超時時間,創建socket連接的超時時間
.setSocketTimeout(2000) //socket讀取數據的超時時間,從服務器獲取數據的超時時間
.build();
}
1. 從連接池中獲取可用連接超時ConnectionRequestTimeout
HttpClient中的要用連接時嘗試從連接池中獲取,若是在等待了一定的時間后還沒有獲取到可用連接(比如連接池中沒有空閑連接了)則會拋出獲取連接超時異常。
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
并發請求的連接數超過了DefaultMaxPerRoute設置。并且在ConnectionRequestTimeout
時間內依舊沒有獲取到可用連接,則會拋出上述異常,解決上述異常的方法就是適當調大一些DefaultMaxPerRoute
和MaxTotal
的大小。
2. 連接目標超時connectionTimeout
指的是連接目標url的連接超時時間,即客服端發送請求到與目標url建立起連接的最大時間。如果在該時間范圍內還沒有建立起連接,則就拋出connectionTimeOut異常。
如測試的時候,將url改為一個不存在的url:“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。
測試的時候的連接url為我本地開啟的一個url,http://localhost:8080/firstTest.htm?method=test,在我這個測試url里,當訪問到這個鏈接時,線程sleep一段時間,來模擬返回response超時。
2.2 KeepAliveStrategy策略
keep-alive詳解 —— 通過使用Keep-alive機制,可以減少tcp連接建立的次數,也以為這可以減少TIME_WAIT狀態連接,以此提高性能和提高HTTP服務器的吞吐率(更少的tcp連接意味著更少的系統內核調用,socket的accept()和close()調用)。但是長時間的tcp連接容易導致系統資源無效占用,配置不當的Keep-alive有事比重復利用連接帶來的損失還更大。所以正確地設置Keep-alive timeout時間非常重要。
Keep-alive:timeout=5,max=100
的含義。
意思是說:過期時間5秒,max是最多100次請求,強制斷掉連接,也就是在timeout時間內每來一個新的請求,max會自動減1,直到為0,強制斷掉連接。
需要注意的是:使用keep-alive要根據業務情況來定,若是少數固定客戶端,長時間高頻次的訪問服務器,啟用keep-client非常合適!
在HttpClient中默認的keepClient策略:
org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy
默認的話,是讀取response中的keep-alive中的timeout參數,若是沒有讀到,那么設置為-1,這個代表無窮,但是這樣設置便存在問題。因為現實中的HTTP服務器配置了在特定不活動周期之后丟掉連接來保存系統資源,往往是不通知客戶端的。
默認的keep-alive策略
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
public static final DefaultConnectionKeepAliveStrategy INSTANCE = new DefaultConnectionKeepAliveStrategy();
@Override
public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
Args.notNull(response, "HTTP response");
final HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
final HeaderElement he = it.nextElement();
final String param = he.getName();
final String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(final NumberFormatException ignore) {
}
}
}
return -1;
}
}
解決方案:可以自定義keep-alive策略,如果沒有讀到,則設置保存連接為60s。
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應由調用者管理,如果客戶端關閉則不會關閉。
httpClientBuilder.setConnectionManagerShared(true);
//設置KeepAlive
ConnectionKeepAliveStrategy myStrategy = 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) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
return 30 * 1000;
}
}
};
httpClientBuilder.setKeepAliveStrategy(myStrategy);
return httpClientBuilder;
}
2.3 Connection eviction policy(連接逐出策略)
當一個連接被釋放到連接池時,它可以保持活動狀態而不能監控socket的狀態和任何I/O事件。如果連接在服務器端被關閉,那么客戶端連接也不能偵測連接狀態中的變化和關閉本端的套接字去做出適當響應。
HttpClient嘗試通過測試連接是否有效來解決該問題,但是它在服務器端關閉,失效的連接檢查不是100%可靠。唯一的解決方案:創建監控線程來回收因為長時間不活動而被認為過期的連接。
public class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
監控線程可以周期地調用ClientConnectionManager#closeExpiredConnections()
方法來關閉所有過期的連接,從連接池中收回關閉的連接。它也可以選擇性調用ClientConnectionManager#closeIdleConnections()
方法來關閉所有已經空閑超過給定時間周期的連接。httpclient參數配置
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應由調用者管理,如果客戶端關閉則不會關閉。
httpClientBuilder.setConnectionManagerShared(true);
//啟動線程,5秒鐘清空一次失效連接
new IdleConnectionMonitorThread(poolingHttpClientConnectionManager).start();
return httpClientBuilder;
}
2.4 HttpClient的重試機制
該參數如果在并發請求量大的請求下,推薦關閉。如果項目量不到,這個默認即可。
HttpClient使用連接池PoolingHttpClientConnectionManager
設置重試策略:org.apache.http.impl.client.DefaultHttpRequestRetryHandler
重試機制的源碼:org.apache.http.impl.execchain.RetryExec#execute
在默認情況下,httpClient會使用默認的重試策略
DefaultHttpRequestRetryHandler
(不管你設置不設置)。
默認策略的構造方法:
public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
this(retryCount, requestSentRetryEnabled, Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
SSLException.class));
}
- retryCount:重試次數;
- requestSentRetryEnabled:如果一個請求重試成功,是否還會被再次重試;
- InterruptedIOException、UnknownHostException、ConnectException、SSLException,發生這4中異常(以及子類異常)不重試;
默認重試策略的校驗方法:org.apache.http.impl.client.DefaultHttpRequestRetryHandler # retryRequest
@Override
public boolean retryRequest(
final IOException exception,
final int executionCount,
final HttpContext context) {
Args.notNull(exception, "Exception parameter");
Args.notNull(context, "HTTP context");
if (executionCount > this.retryCount) {
// Do not retry if over max retry count
return false;
}
if (this.nonRetriableClasses.contains(exception.getClass())) {
return false;
}
for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) {
if (rejectException.isInstance(exception)) {
return false;
}
}
final HttpClientContext clientContext = HttpClientContext.adapt(context);
final HttpRequest request = clientContext.getRequest();
//同一個請求在異步任務重已經被終止,則不進行重試
if(requestIsAborted(request)){
return false;
}
//判斷請求是否是冪等的
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
//如果請求未發送成功,或者允許發送成功依舊可以發送,便可以重試
if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) {
// Retry if the request has not been sent fully or
// if it's OK to retry methods that have been sent
return true;
}
// otherwise do not retry
return false;
}
關于默認的重試策略:
- 如果重試超過3次,則不進行重試;
- 如果重試是特殊異常及其子類,則不重試(見下文);
- 同一個請求在異步任務被終止,則不請求;
- 冪等的方法可以進行重試,比如Get;
- 如果請求未被發送成功,可以被重試;
如何判斷請求是否發送成功?
源碼:org.apache.http.protocol.HttpCoreContext # isRequestSent
根據http.request_sent
參數來判斷是否發送成功。
RetryExec底層通信使用的是MainClientExec,而MainClientExec底層便調用的是HttpRequestExecutor.doSendRequest()。
故http.request_sent
參數的設置,是通過HttpRequestExecutor.doSendRequest()方法設置的。
不重試的異常
- InterruptedIOException,線程中斷異常
- UnknownHostException,找不到對應host
- ConnectException,找到了host但是建立連接失敗。
- SSLException,https認證異常
另外,我們還經常會提到兩種超時,連接超時與讀超時:
- java.net.SocketTimeoutException: Read timed out
- java.net.SocketTimeoutException: connect timed out
這兩種超時都是SocketTimeoutException,繼承自InterruptedIOException,屬于上面的第1種線程中斷異常,不會進行重試。
不重試的冪等請求
默認重試類中:handleAsIdempotent(request)
會校驗請求是否是冪等的。默認實現:
public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
protected boolean handleAsIdempotent(final HttpRequest request) {
return !(request instanceof HttpEntityEnclosingRequest);
}
}
判斷請求是否屬于HttpEntityEnclosingRequest
類。
這就會導致若是post請求,那么handleAsIdempotent
方法會返回false,即不重試。
如何禁止重試
在HttpClinetBuilder中,其Build()方法中選擇了RetryExec執行器時,是默認開啟重試策略。
故我們可以在構建httpClient實例的時候手動禁止掉即可。
httpClientBuilder.disableAutomaticRetries();
如何自定義重試策略
只需要實現org.apache.http.client.HttpRequestRetryHandler
接口,重新里面的方法即可。
而重試策略的源碼是在org.apache.http.impl.execchain.RetryExec#execute
實現的。
httpClientBuilder.setRetryHandler(new MyHttpRequestRetryHandler());
2.5 設置個性化的請求參數
因為我們在配置文件中,配置了默認的socketTimeout(建立連接的最大時間,即響應超時時間),但是實際業務中,不同的請求有著不同的響應超時時間。如何為不同的業務設置不同的超時時間呢?
我們知道,實際上我們注入的CloseableHttpClient
是一個抽象類,實際上,他將org.apache.http.impl.client.InternalHttpClient
類型注入進來,那么在我們使用org.apache.http.client.methods.HttpRequestBase
(注:httpPost/httpGet的共同父類)發送請求時,可以單獨的設置RequestConfig
參數。
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());獲取RequestConfig.Builder對象,以便設置個性化參數。
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設置個性化配置
RequestConfig config = custom.setSocketTimeout(socketTimeout).build();
request.setConfig(config);
}
ResponseHandler<String> handler = new BasicResponseHandler();
String response = httpClient.execute(request, handler);
return response;
}
}
2.6 HttpClient響應數據處理
EntityUtils.consume將釋放httpEntity持有的所有資源,這實際上意味著釋放任何基礎流并將連接對象放回到池中(在連接池時多線程的情況下),或者釋放連接管理器以便處理下一個請求。
源碼:org.apache.http.impl.client.CloseableHttpClient # execute
若是獲取自定義響應實體,則實現org.apache.http.client.ResponseHandler
接口。
處理響應的方法:
@Test
public void test1() throws IOException, InterruptedException {
HttpPost httpPost = new HttpPost("http://www.baidu.com");
httpPost.setConfig(requestConfig);
Map<String, String> innerReq = new HashMap<>();
innerReq.put("XX", "data1");
innerReq.put("YY", "data2");
String innerReqJson = JSONObject.toJSONString(innerReq);
StringEntity entity = new StringEntity(innerReqJson, "UTF-8");
httpPost.addHeader("content-type", "application/json;charset=UTF-8");
httpPost.setEntity(entity);
//執行請求
CloseableHttpResponse execute = closeableHttpClient.execute(httpPost);
//設置返回數據
String res = EntityUtils.toString(execute.getEntity(), "UTF-8");
//關閉資源
EntityUtils.consume(execute.getEntity());
log.info(res);
}
關閉資源
為什么筆者使用EntityUtils.consume(httpEntity);?(Why did the author use EntityUtils.consume(httpEntity);?)
EntityUtils.consume(execute.getEntity());
(新)使用ResponseHandler處理響應數據
無論請求執行成功還是導致異常,HttpClient都會自動確保將連接釋放回連接管理器。
@Test
public void test() throws IOException, InterruptedException {
HttpPost httpPost = new HttpPost("http://www.baidu.com");
httpPost.setConfig(requestConfig);
Map<String, String> innerReq = new HashMap<>();
innerReq.put("XX", "data1");
innerReq.put("YY", "data2");
String innerReqJson = JSONObject.toJSONString(innerReq);
StringEntity entity = new StringEntity(innerReqJson, "UTF-8");
httpPost.addHeader("content-type", "application/json;charset=UTF-8");
httpPost.setEntity(entity);
//自定義ResponseHandler
ResponseHandler<ResponseVo> handler = new ResponseHandler<ResponseVo>() {
@Override
public ResponseVo handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
final StatusLine statusLine = response.getStatusLine();
final HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
EntityUtils.consume(entity);
throw new HttpResponseException(statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if (entity == null) {
throw new ClientProtocolException("異常!");
}
String res = EntityUtils.toString(entity);
ResponseVo responseVo = JSON.parseObject(res, ResponseVo.class);
return responseVo;
}
};
//無論請求執行成功還是導致異常,HttpClient都會自動確保將連接釋放回連接管理器。
ResponseHandler<String> responseHandler = new BasicResponseHandler();
// String execute1 = closeableHttpClient.execute(httpPost, responseHandler);
ResponseVo execute = closeableHttpClient.execute(httpPost, handler);
log.info(JSON.toJSONString(execute));
}
2.7 請求工具類
接收POST請求:
public static String doPost(String url, Object paramsObj, int socketTimeout) throws IOException {
HttpPost post = new HttpPost(url);
StringEntity entity = new StringEntity(JSONObject.toJSONString(paramsObj), "UTF-8");
post.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
post.setEntity(entity);
return doHttp(post, socketTimeout);
}
接收GET請求:
public static String doGet(String url, Map<String, String> params, int socketTimeout) throws IOException, URISyntaxException {
URIBuilder uriBuilder = new URIBuilder(url);
uriBuilder.setCharset(Consts.UTF_8).build();
if (params != null) {
params.forEach(uriBuilder::addParameter);
}
HttpGet httpGet = new HttpGet(uriBuilder.build());
//設置請求頭
httpGet.addHeader(HttpHeaders.CONTENT_TYPE, "text/html;charset=UTF-8");
return doHttp(httpGet, socketTimeout);
}
公共處理類:
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設置個性化配置
RequestConfig config = custom.setSocketTimeout(socketTimeout).build();
request.setConfig(config);
}
ResponseHandler<String> handler = new BasicResponseHandler();
long startPoint = System.currentTimeMillis();
String response = httpClient.execute(request, handler);
log.info("請求耗時【{}】, 接口返回信息【{}】", System.currentTimeMillis() - startPoint, response);
return response;
}
附錄:
httpClient配置:
@Configuration
public class HttpClientConfig {
@Autowired
private HttpClientProperties httpClientProperties;
/**
* 顯示修改httpClient連接池參數,注:若未顯示設置,應該有默認配置!
*
* @return
*/
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
//創建出來的對象,已經設置了:協議Http和Https對應的處理Socket鏈接工廠對象。
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
httpClientConnectionManager.setDefaultMaxPerRoute(httpClientProperties.getDefaultMaxPerRoute());
httpClientConnectionManager.setMaxTotal(httpClientProperties.getMaxTotal());
httpClientConnectionManager.setValidateAfterInactivity(httpClientProperties.getValidateAfterInactivity());
return httpClientConnectionManager;
}
//設置網絡配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(httpClientProperties.getConnectionRequestTimeout()) //從鏈接池獲取連接的超時時間
.setConnectTimeout(httpClientProperties.getConnectTimeout()) //與服務器連接超時時間,創建socket連接的超時時間
.setSocketTimeout(httpClientProperties.getSocketTimeout()) //socket讀取數據的超時時間,從服務器獲取數據的超時時間
// .setSocketTimeout(1) //socket讀取數據的超時時間,從服務器獲取數據的超時時間
// .setExpectContinueEnabled(true) //設置是否開啟 客戶端在發送Request Message之前,先判斷服務器是否愿意接受客戶端發送的消息主體
.build();
}
/**
* 實例化連接池,設置連接池管理器
*
* @param poolingHttpClientConnectionManager
* @return
*/
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應由調用者管理,如果客戶端關閉則不會關閉。
httpClientBuilder.setConnectionManagerShared(true);
//設置Keep-Alive
ConnectionKeepAliveStrategy myStrategy = 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) {
}
}
}
HttpHost target = (HttpHost) context.getAttribute(
HttpClientContext.HTTP_TARGET_HOST);
if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
// Keep alive for 5 seconds only
return 5 * 1000;
} else {
// otherwise keep alive for 30 seconds
return 30 * 1000;
}
}
};
httpClientBuilder.setKeepAliveStrategy(myStrategy);
// httpClientBuilder.setRetryHandler(new MyHttpRequestRetryHandler());
// httpClientBuilder.disableAutomaticRetries();
new IdleConnectionMonitorThread(poolingHttpClientConnectionManager).start();//啟動線程,5秒鐘清空一次失效連接
return httpClientBuilder;
}
@Bean
public CloseableHttpClient getCloseableHttpClient(HttpClientBuilder httpClientBuilder) {
return httpClientBuilder.build();
}
}
定時清除線程
@Slf4j
public class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
log.info("【定時清除過期連接開始...】");
// 關閉超時的連接
connMgr.closeExpiredConnections();
// 關閉空閑時間大于30s的連接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
spring:
http-pool:
# 連接池最大連接數
max-total: 3000
# 每個rount請求的最大連接數
default-max-per-route: 20
# 空閑多長時間(毫秒)來校驗連接的有效性
validate-after-inactivity: 2000
# 建立連接的最大超時時間(毫秒)
connect-timeout: 20000
# 獲取連接的最大超時時間(毫秒)
connection-request-timeout: 20000
# 與服務端保持連接的最大時間(毫秒)
socket-timeout: 20000
@ConfigurationProperties(prefix = "spring.http-pool")
public class HttpClientProperties {
//默認配置
private int defaultMaxPerRoute = 2;
private int maxTotal = 20;
private int validateAfterInactivity = 2000;
private int connectTimeout = 2000;
private int connectionRequestTimeout = 20000;
private int socketTimeout = 20000;
}
工具類:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Consts;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.BeanUtils;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @program: springboot
* @description: httpClient通信工具類
* @author: xueruiye
* @create: 2019-08-13 17:18
* <p>
* 注:設置httpClient的工具類。提供了get和post訪問的靜態方法。
* get請求 Content-Type==text/html;charset=UTF-8
* post請求 Content-Type=application/json;charset=UTF-8
* 可以靈活的設置socket-timeout(socket連接時間,即超時時間,單位毫秒!)
*/
@Slf4j
public class HttpClientUtils {
private static CloseableHttpClient httpClient = SpringContextUtil.getBean("customCloseableHttpClient", CloseableHttpClient.class);
/**
* get 請求 Content-Type==text/html;charset=UTF-8
*
* @param url url地址
* @param paramsObj params參數組成的Object對象
* @return
* @throws IOException
* @throws URISyntaxException
*/
public static <T> String doGet(String url, Object paramsObj) throws IOException, URISyntaxException {
Map<String, String> params = JSON.parseObject(JSON.toJSONString(paramsObj), Map.class);
return doGet(url, params, -1);
}
public static <T> String doGet(String url, Object paramsObj, int socketTimeout) throws IOException, URISyntaxException {
Map<String, String> params = JSON.parseObject(JSON.toJSONString(paramsObj), Map.class);
return doGet(url, params, socketTimeout);
}
/**
* post調用 使用配置文件中配置的超時時間
*
* @param url 請求地址
* @param paramsObj 請求實體
* @param responseType 請求內容 例子:new TypeReference<List<Account>>(){}
* @param <T>
* @return
* @throws IOException
*/
public static <T> T doPost(String url, Object paramsObj, TypeReference<T> responseType) throws IOException {
return doPost(url, paramsObj, responseType, -1);
}
public static String doPost(String url, Object paramsObj) throws IOException {
return doPost(url, paramsObj, -1);
}
/**
* post請求 Content-Type=application/json;charset=UTF-8
*
* @param url url地址
* @param paramsObj 請求參數域
* @param responseType 響應對象類型
* @param socketTimeout 超時時間
* @param <T>
* @return 響應實體對應的內容
* @throws IOException
*/
public static <T> T doPost(String url, Object paramsObj, TypeReference<T> responseType, int socketTimeout) throws IOException {
String responseContent = doPost(url, paramsObj, socketTimeout);
if (StringUtils.isBlank(responseContent)) {
return null;
}
T response = JSONObject.parseObject(responseContent, responseType);
return response;
}
/**
* @param url
* @param paramsObj
* @param socketTimeout
* @return
* @throws IOException
*/
public static String doPost(String url, Object paramsObj, int socketTimeout) throws IOException {
HttpPost post = new HttpPost(url);
//若上送String類型對象,無需進行String類型轉換
String paramsStr = paramsObj instanceof String ? (String) paramsObj : JSONObject.toJSONString(paramsObj);
StringEntity entity = new StringEntity(paramsStr, "UTF-8");
post.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
post.setEntity(entity);
return doHttp(post, socketTimeout);
}
/**
* get 請求 Content-Type==text/html;charset=UTF-8
*
* @param url url地址
* @param params params參數組成的Map對象
* @return
* @throws IOException
* @throws URISyntaxException
*/
public static String doGet(String url, Map<String, String> params) throws IOException, URISyntaxException {
return doGet(url, params, -1);
}
public static String doGet(String url, Map<String, String> params, int socketTimeout) throws IOException, URISyntaxException {
URIBuilder uriBuilder = new URIBuilder(url);
uriBuilder.setCharset(Consts.UTF_8).build();
if (params != null) {
// Set<String> keys = params.keySet();
// for (String key : keys) {
// uriBuilder.addParameter(key, params.get(key));
// }
params.forEach(uriBuilder::addParameter);
}
HttpGet httpGet = new HttpGet(uriBuilder.build());
//設置請求頭
httpGet.addHeader(HttpHeaders.CONTENT_TYPE, "text/html;charset=UTF-8");
return doHttp(httpGet, socketTimeout);
}
/**
* 實際上調用遠程的方法
*
* @param request httpGet/httpPost的共同父類
* @param socketTimeout 超時時間
* @return
* @throws IOException
*/
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設置個性化配置
RequestConfig config = custom.setSocketTimeout(socketTimeout).build();
request.setConfig(config);
}
ResponseHandler<String> handler = new BasicResponseHandler();
long startPoint = System.currentTimeMillis();
String response = httpClient.execute(request, handler);
log.info("請求耗時【{}】, 接口返回信息【{}】", System.currentTimeMillis() - startPoint, response);
return response;
}
}
文章參考
1. 官方文檔
類PoolingHttpClientConnectionManager 官網API文檔
httpclient源碼分析之 PoolingHttpClientConnectionManager 獲取連接
2. 相關博客
使用PoolingHttpClientConnectionManager解決友…
HttpClient.DefaultRequestHeaders.ExpectContinue。 ExpectContinue的用途是什么,在什么條件下它被設置為true或false。
理解HTTP協議中的 Expect: 100-continue
java.lang.IllegalStateException: Connection pool shut down 的解決方案