JAVA && Spring && SpringBoot2.x — 學(xué)習(xí)目錄
目錄
- 連接池的設(shè)置。
- 獲取連接超時時間、建立連接超時時間、保持連接超時時間的設(shè)置。
- 長連接策略的設(shè)置。
- 連接逐出策略的設(shè)置。
- 重試機制的設(shè)置。
- 個性化請求參數(shù)的設(shè)置。
- 附錄。
序
HttpClient可以用來提供高效的、最新的、功能豐富的支持 HTTP 協(xié)議的客戶端編程工具包,并且它支持 HTTP 協(xié)議最新的版本和建議。
使用HttpClient發(fā)送請求和接收響應(yīng)的步驟:
- 創(chuàng)建CloseableHttpClient對象;
- 創(chuàng)建請求方法實例,并指定請求URL。例:如果要發(fā)送Get請求,創(chuàng)建HttpGet對象;如果要發(fā)送POST請求,創(chuàng)建HttpPost對象;
- 如果需要發(fā)送參數(shù),則調(diào)用setEntity(HttpEntity entity)方法來設(shè)置參數(shù);
- 調(diào)用HttpGet/HttpPost對象的setHeader(String name,String value)方法設(shè)置header信息,或者調(diào)用setHeader(Header[] headers)設(shè)置一組header參數(shù);
- 調(diào)用CloseableHttpClient對象的execute(HttpUriRequest request)發(fā)送請求,該方法返回一個CloseableHttpResponse;
- 調(diào)用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務(wù)器的響應(yīng)內(nèi)容。程序可通過該對象獲取服務(wù)器的響應(yīng)內(nèi)容;調(diào)用CloseableHttpResponse的getAllHeaders()、getHeaders(String name)等方法可獲取服務(wù)器的響應(yīng)頭;
- 釋放連接。無論執(zhí)行方法是否成功,都必須釋放連接
1. 引入Maven依賴
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
1. HttpClient連接池分析
PoolingHttpClientConnectionManager是一個HttpClientConnection的連接池,可以為多線程提供并發(fā)請求服務(wù)。主要是分配連接,回收連接。同一個遠程請求,會優(yōu)先使用連接池提供的空閑的長連接。
源碼位置:org.apache.http.impl.conn.PoolingHttpClientConnectionManager
默認構(gòu)造方法:
/**
* @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:連接池的最大連接數(shù)。
- defaultMaxPreRount:每個Rount(遠程)請求最大的連接數(shù)。
- setValidateAfterInactivity:連接空閑多長時間(單位:毫秒)進行檢查。
顯示的調(diào)整連接池參數(shù):
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必須知道的參數(shù)設(shè)置及代碼寫法、存在的風險
1.1 MaxTotal和DefaultMaxPerRoute的區(qū)別
參數(shù)配置:MaxTotal=100,DefaultMaxPerRoute=5
服務(wù)器端睡眠2秒。該圖是客戶端的響應(yīng)信息截圖。

可以看到,只有5筆請求并發(fā)的調(diào)用遠程服務(wù)端,得到響應(yīng)之后。再次有5筆請求調(diào)用服務(wù)端。
- MaxtTotal是整個池子的大??;
- DefaultMaxPerRoute是根據(jù)連接到的主機對MaxTotal的一個細分;比如:
MaxtTotal=400 DefaultMaxPerRoute=200
而我只連接到http://sishuok.com時,到這個主機的并發(fā)最多只有200;而不是400;
而我連接到http://sishuok.com 和 http://qq.com時,到每個主機的并發(fā)最多只有200;即加起來是400(但不能超過400);所以起作用的設(shè)置是DefaultMaxPerRoute。
2. SpringBoot集成HttpClient
2.1 超時時間設(shè)置
httpClient內(nèi)部有三個超時時間設(shè)置:獲取連接的超時時間、建立連接的超時時間、讀取數(shù)據(jù)超時時間。
//設(shè)置網(wǎng)絡(luò)配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(2000) //從鏈接池獲取連接的超時時間
.setConnectTimeout(2000) //與服務(wù)器連接超時時間,創(chuàng)建socket連接的超時時間
.setSocketTimeout(2000) //socket讀取數(shù)據(jù)的超時時間,從服務(wù)器獲取數(shù)據(jù)的超時時間
.build();
}
1. 從連接池中獲取可用連接超時ConnectionRequestTimeout
HttpClient中的要用連接時嘗試從連接池中獲取,若是在等待了一定的時間后還沒有獲取到可用連接(比如連接池中沒有空閑連接了)則會拋出獲取連接超時異常。
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
并發(fā)請求的連接數(shù)超過了DefaultMaxPerRoute設(shè)置。并且在ConnectionRequestTimeout時間內(nèi)依舊沒有獲取到可用連接,則會拋出上述異常,解決上述異常的方法就是適當調(diào)大一些DefaultMaxPerRoute和MaxTotal的大小。
2. 連接目標超時connectionTimeout
指的是連接目標url的連接超時時間,即客服端發(fā)送請求到與目標url建立起連接的最大時間。如果在該時間范圍內(nèi)還沒有建立起連接,則就拋出connectionTimeOut異常。
如測試的時候,將url改為一個不存在的url:“http://test.com” , 超時時間3000ms過后,系統(tǒng)報出異常: org.apache.commons.httpclient.ConnectTimeoutException:The host did not accept the connection within timeout of 3000 ms
3. 等待響應(yīng)超時(讀取數(shù)據(jù)超時)socketTimeout
連接上一個url后,獲取response的返回等待時間 ,即在與目標url建立連接后,等待放回response的最大時間,在規(guī)定時間內(nèi)沒有返回響應(yīng)的話就拋出SocketTimeout。
測試的時候的連接url為我本地開啟的一個url,http://localhost:8080/firstTest.htm?method=test,在我這個測試url里,當訪問到這個鏈接時,線程sleep一段時間,來模擬返回response超時。
2.2 KeepAliveStrategy策略
keep-alive詳解 —— 通過使用Keep-alive機制,可以減少tcp連接建立的次數(shù),也以為這可以減少TIME_WAIT狀態(tài)連接,以此提高性能和提高HTTP服務(wù)器的吞吐率(更少的tcp連接意味著更少的系統(tǒng)內(nèi)核調(diào)用,socket的accept()和close()調(diào)用)。但是長時間的tcp連接容易導(dǎo)致系統(tǒng)資源無效占用,配置不當?shù)腒eep-alive有事比重復(fù)利用連接帶來的損失還更大。所以正確地設(shè)置Keep-alive timeout時間非常重要。
Keep-alive:timeout=5,max=100的含義。
意思是說:過期時間5秒,max是最多100次請求,強制斷掉連接,也就是在timeout時間內(nèi)每來一個新的請求,max會自動減1,直到為0,強制斷掉連接。
需要注意的是:使用keep-alive要根據(jù)業(yè)務(wù)情況來定,若是少數(shù)固定客戶端,長時間高頻次的訪問服務(wù)器,啟用keep-client非常合適!
在HttpClient中默認的keepClient策略:
org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy
默認的話,是讀取response中的keep-alive中的timeout參數(shù),若是沒有讀到,那么設(shè)置為-1,這個代表無窮,但是這樣設(shè)置便存在問題。因為現(xiàn)實中的HTTP服務(wù)器配置了在特定不活動周期之后丟掉連接來保存系統(tǒng)資源,往往是不通知客戶端的。
默認的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策略,如果沒有讀到,則設(shè)置保存連接為60s。
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設(shè)置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設(shè)置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應(yīng)由調(diào)用者管理,如果客戶端關(guān)閉則不會關(guān)閉。
httpClientBuilder.setConnectionManagerShared(true);
//設(shè)置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(連接逐出策略)
當一個連接被釋放到連接池時,它可以保持活動狀態(tài)而不能監(jiān)控socket的狀態(tài)和任何I/O事件。如果連接在服務(wù)器端被關(guān)閉,那么客戶端連接也不能偵測連接狀態(tài)中的變化和關(guān)閉本端的套接字去做出適當響應(yīng)。
HttpClient嘗試通過測試連接是否有效來解決該問題,但是它在服務(wù)器端關(guān)閉,失效的連接檢查不是100%可靠。唯一的解決方案:創(chuàng)建監(jiān)控線程來回收因為長時間不活動而被認為過期的連接。
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();
}
}
}
監(jiān)控線程可以周期地調(diào)用ClientConnectionManager#closeExpiredConnections()方法來關(guān)閉所有過期的連接,從連接池中收回關(guān)閉的連接。它也可以選擇性調(diào)用ClientConnectionManager#closeIdleConnections()方法來關(guān)閉所有已經(jīng)空閑超過給定時間周期的連接。httpclient參數(shù)配置
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設(shè)置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設(shè)置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應(yīng)由調(diào)用者管理,如果客戶端關(guān)閉則不會關(guān)閉。
httpClientBuilder.setConnectionManagerShared(true);
//啟動線程,5秒鐘清空一次失效連接
new IdleConnectionMonitorThread(poolingHttpClientConnectionManager).start();
return httpClientBuilder;
}
2.4 HttpClient的重試機制
該參數(shù)如果在并發(fā)請求量大的請求下,推薦關(guān)閉。如果項目量不到,這個默認即可。
HttpClient使用連接池PoolingHttpClientConnectionManager
設(shè)置重試策略:org.apache.http.impl.client.DefaultHttpRequestRetryHandler
重試機制的源碼:org.apache.http.impl.execchain.RetryExec#execute
在默認情況下,httpClient會使用默認的重試策略
DefaultHttpRequestRetryHandler(不管你設(shè)置不設(shè)置)。
默認策略的構(gòu)造方法:
public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) {
this(retryCount, requestSentRetryEnabled, Arrays.asList(
InterruptedIOException.class,
UnknownHostException.class,
ConnectException.class,
SSLException.class));
}
- retryCount:重試次數(shù);
- requestSentRetryEnabled:如果一個請求重試成功,是否還會被再次重試;
- InterruptedIOException、UnknownHostException、ConnectException、SSLException,發(fā)生這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();
//同一個請求在異步任務(wù)重已經(jīng)被終止,則不進行重試
if(requestIsAborted(request)){
return false;
}
//判斷請求是否是冪等的
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
//如果請求未發(fā)送成功,或者允許發(fā)送成功依舊可以發(fā)送,便可以重試
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;
}
關(guān)于默認的重試策略:
- 如果重試超過3次,則不進行重試;
- 如果重試是特殊異常及其子類,則不重試(見下文);
- 同一個請求在異步任務(wù)被終止,則不請求;
- 冪等的方法可以進行重試,比如Get;
- 如果請求未被發(fā)送成功,可以被重試;
如何判斷請求是否發(fā)送成功?
源碼:org.apache.http.protocol.HttpCoreContext # isRequestSent根據(jù)http.request_sent參數(shù)來判斷是否發(fā)送成功。
RetryExec底層通信使用的是MainClientExec,而MainClientExec底層便調(diào)用的是HttpRequestExecutor.doSendRequest()。
故http.request_sent參數(shù)的設(shè)置,是通過HttpRequestExecutor.doSendRequest()方法設(shè)置的。
不重試的異常
- InterruptedIOException,線程中斷異常
- UnknownHostException,找不到對應(yīng)host
- ConnectException,找到了host但是建立連接失敗。
- SSLException,https認證異常
另外,我們還經(jīng)常會提到兩種超時,連接超時與讀超時:
- java.net.SocketTimeoutException: Read timed out
- java.net.SocketTimeoutException: connect timed out
這兩種超時都是SocketTimeoutException,繼承自InterruptedIOException,屬于上面的第1種線程中斷異常,不會進行重試。
不重試的冪等請求
默認重試類中:handleAsIdempotent(request)會校驗請求是否是冪等的。默認實現(xiàn):
public class DefaultHttpRequestRetryHandler implements HttpRequestRetryHandler {
protected boolean handleAsIdempotent(final HttpRequest request) {
return !(request instanceof HttpEntityEnclosingRequest);
}
}
判斷請求是否屬于HttpEntityEnclosingRequest類。

這就會導(dǎo)致若是post請求,那么handleAsIdempotent方法會返回false,即不重試。
如何禁止重試
在HttpClinetBuilder中,其Build()方法中選擇了RetryExec執(zhí)行器時,是默認開啟重試策略。
故我們可以在構(gòu)建httpClient實例的時候手動禁止掉即可。
httpClientBuilder.disableAutomaticRetries();
如何自定義重試策略
只需要實現(xiàn)org.apache.http.client.HttpRequestRetryHandler接口,重新里面的方法即可。
而重試策略的源碼是在org.apache.http.impl.execchain.RetryExec#execute實現(xiàn)的。
httpClientBuilder.setRetryHandler(new MyHttpRequestRetryHandler());
2.5 設(shè)置個性化的請求參數(shù)
因為我們在配置文件中,配置了默認的socketTimeout(建立連接的最大時間,即響應(yīng)超時時間),但是實際業(yè)務(wù)中,不同的請求有著不同的響應(yīng)超時時間。如何為不同的業(yè)務(wù)設(shè)置不同的超時時間呢?
我們知道,實際上我們注入的CloseableHttpClient是一個抽象類,實際上,他將org.apache.http.impl.client.InternalHttpClient類型注入進來,那么在我們使用org.apache.http.client.methods.HttpRequestBase(注:httpPost/httpGet的共同父類)發(fā)送請求時,可以單獨的設(shè)置RequestConfig參數(shù)。
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());獲取RequestConfig.Builder對象,以便設(shè)置個性化參數(shù)。
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設(shè)置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設(shè)置個性化配置
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響應(yīng)數(shù)據(jù)處理
EntityUtils.consume將釋放httpEntity持有的所有資源,這實際上意味著釋放任何基礎(chǔ)流并將連接對象放回到池中(在連接池時多線程的情況下),或者釋放連接管理器以便處理下一個請求。
源碼:org.apache.http.impl.client.CloseableHttpClient # execute
若是獲取自定義響應(yīng)實體,則實現(xiàn)org.apache.http.client.ResponseHandler接口。
處理響應(yīng)的方法:
@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);
//執(zhí)行請求
CloseableHttpResponse execute = closeableHttpClient.execute(httpPost);
//設(shè)置返回數(shù)據(jù)
String res = EntityUtils.toString(execute.getEntity(), "UTF-8");
//關(guān)閉資源
EntityUtils.consume(execute.getEntity());
log.info(res);
}
關(guān)閉資源
為什么筆者使用EntityUtils.consume(httpEntity);?(Why did the author use EntityUtils.consume(httpEntity);?)
EntityUtils.consume(execute.getEntity());
(新)使用ResponseHandler處理響應(yīng)數(shù)據(jù)
無論請求執(zhí)行成功還是導(dǎo)致異常,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;
}
};
//無論請求執(zhí)行成功還是導(dǎo)致異常,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());
//設(shè)置請求頭
httpGet.addHeader(HttpHeaders.CONTENT_TYPE, "text/html;charset=UTF-8");
return doHttp(httpGet, socketTimeout);
}
公共處理類:
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設(shè)置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設(shè)置個性化配置
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連接池參數(shù),注:若未顯示設(shè)置,應(yīng)該有默認配置!
*
* @return
*/
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
//創(chuàng)建出來的對象,已經(jīng)設(shè)置了:協(xié)議Http和Https對應(yīng)的處理Socket鏈接工廠對象。
PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager();
httpClientConnectionManager.setDefaultMaxPerRoute(httpClientProperties.getDefaultMaxPerRoute());
httpClientConnectionManager.setMaxTotal(httpClientProperties.getMaxTotal());
httpClientConnectionManager.setValidateAfterInactivity(httpClientProperties.getValidateAfterInactivity());
return httpClientConnectionManager;
}
//設(shè)置網(wǎng)絡(luò)配置器
@Bean
public RequestConfig requestConfig(){
return RequestConfig.custom().setConnectionRequestTimeout(httpClientProperties.getConnectionRequestTimeout()) //從鏈接池獲取連接的超時時間
.setConnectTimeout(httpClientProperties.getConnectTimeout()) //與服務(wù)器連接超時時間,創(chuàng)建socket連接的超時時間
.setSocketTimeout(httpClientProperties.getSocketTimeout()) //socket讀取數(shù)據(jù)的超時時間,從服務(wù)器獲取數(shù)據(jù)的超時時間
// .setSocketTimeout(1) //socket讀取數(shù)據(jù)的超時時間,從服務(wù)器獲取數(shù)據(jù)的超時時間
// .setExpectContinueEnabled(true) //設(shè)置是否開啟 客戶端在發(fā)送Request Message之前,先判斷服務(wù)器是否愿意接受客戶端發(fā)送的消息主體
.build();
}
/**
* 實例化連接池,設(shè)置連接池管理器
*
* @param poolingHttpClientConnectionManager
* @return
*/
@Bean
public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
//設(shè)置連接池
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
//設(shè)置超時時間
httpClientBuilder.setDefaultRequestConfig(requestConfig());
//定義連接管理器將由多個客戶端實例共享。如果連接管理器是共享的,則其生命周期應(yīng)由調(diào)用者管理,如果客戶端關(guān)閉則不會關(guān)閉。
httpClientBuilder.setConnectionManagerShared(true);
//設(shè)置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("【定時清除過期連接開始...】");
// 關(guān)閉超時的連接
connMgr.closeExpiredConnections();
// 關(guān)閉空閑時間大于30s的連接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
spring:
http-pool:
# 連接池最大連接數(shù)
max-total: 3000
# 每個rount請求的最大連接數(shù)
default-max-per-route: 20
# 空閑多長時間(毫秒)來校驗連接的有效性
validate-after-inactivity: 2000
# 建立連接的最大超時時間(毫秒)
connect-timeout: 20000
# 獲取連接的最大超時時間(毫秒)
connection-request-timeout: 20000
# 與服務(wù)端保持連接的最大時間(毫秒)
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>
* 注:設(shè)置httpClient的工具類。提供了get和post訪問的靜態(tài)方法。
* get請求 Content-Type==text/html;charset=UTF-8
* post請求 Content-Type=application/json;charset=UTF-8
* 可以靈活的設(shè)置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參數(shù)組成的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調(diào)用 使用配置文件中配置的超時時間
*
* @param url 請求地址
* @param paramsObj 請求實體
* @param responseType 請求內(nèi)容 例子: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 請求參數(shù)域
* @param responseType 響應(yīng)對象類型
* @param socketTimeout 超時時間
* @param <T>
* @return 響應(yīng)實體對應(yīng)的內(nèi)容
* @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類型轉(zhuǎn)換
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參數(shù)組成的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());
//設(shè)置請求頭
httpGet.addHeader(HttpHeaders.CONTENT_TYPE, "text/html;charset=UTF-8");
return doHttp(httpGet, socketTimeout);
}
/**
* 實際上調(diào)用遠程的方法
*
* @param request httpGet/httpPost的共同父類
* @param socketTimeout 超時時間
* @return
* @throws IOException
*/
private static String doHttp(HttpRequestBase request, int socketTimeout) throws IOException {
//設(shè)置超時時間
if (socketTimeout > 0) {
//獲取原有配置
//實際注入類型org.apache.http.impl.client.InternalHttpClient
Configurable configClient = (Configurable) httpClient;
RequestConfig.Builder custom = RequestConfig.copy(configClient.getConfig());
//設(shè)置個性化配置
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 官網(wǎng)API文檔
httpclient源碼分析之 PoolingHttpClientConnectionManager 獲取連接
2. 相關(guān)博客
使用PoolingHttpClientConnectionManager解決友…
HttpClient.DefaultRequestHeaders.ExpectContinue。 ExpectContinue的用途是什么,在什么條件下它被設(shè)置為true或false。
理解HTTP協(xié)議中的 Expect: 100-continue
java.lang.IllegalStateException: Connection pool shut down 的解決方案