SpringBoot2.x集成HttpClient

JAVA && Spring && SpringBoot2.x — 學習目錄

目錄

  1. 連接池的設置。
  2. 獲取連接超時時間、建立連接超時時間、保持連接超時時間的設置。
  3. 長連接策略的設置。
  4. 連接逐出策略的設置。
  5. 重試機制的設置。
  6. 個性化請求參數的設置。
  7. 附錄。

HttpClient可以用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,并且它支持 HTTP 協議最新的版本和建議。

使用HttpClient發送請求和接收響應的步驟:

  1. 創建CloseableHttpClient對象;
  2. 創建請求方法實例,并指定請求URL。例:如果要發送Get請求,創建HttpGet對象;如果要發送POST請求,創建HttpPost對象;
  3. 如果需要發送參數,則調用setEntity(HttpEntity entity)方法來設置參數;
  4. 調用HttpGet/HttpPost對象的setHeader(String name,String value)方法設置header信息,或者調用setHeader(Header[] headers)設置一組header參數;
  5. 調用CloseableHttpClient對象的execute(HttpUriRequest request)發送請求,該方法返回一個CloseableHttpResponse;
  6. 調用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務器的響應內容。程序可通過該對象獲取服務器的響應內容;調用CloseableHttpResponse的getAllHeaders()、getHeaders(String name)等方法可獲取服務器的響應頭;
  7. 釋放連接。無論執行方法是否成功,都必須釋放連接

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秒。該圖是客戶端的響應信息截圖。

并發響應截圖.png

可以看到,只有5筆請求并發的調用遠程服務端,得到響應之后。再次有5筆請求調用服務端。

  1. MaxtTotal是整個池子的大小;
  2. DefaultMaxPerRoute是根據連接到的主機對MaxTotal的一個細分;比如:
    MaxtTotal=400 DefaultMaxPerRoute=200
    而我只連接到http://sishuok.com時,到這個主機的并發最多只有200;而不是400;
    而我連接到http://sishuok.comhttp://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時間內依舊沒有獲取到可用連接,則會拋出上述異常,解決上述異常的方法就是適當調大一些DefaultMaxPerRouteMaxTotal的大小。

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));
    }
  1. retryCount:重試次數;
  2. requestSentRetryEnabled:如果一個請求重試成功,是否還會被再次重試;
  3. 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;
    }

關于默認的重試策略:

  1. 如果重試超過3次,則不進行重試;
  2. 如果重試是特殊異常及其子類,則不重試(見下文);
  3. 同一個請求在異步任務被終止,則不請求;
  4. 冪等的方法可以進行重試,比如Get;
  5. 如果請求未被發送成功,可以被重試;

如何判斷請求是否發送成功?

源碼:org.apache.http.protocol.HttpCoreContext # isRequestSent根據http.request_sent參數來判斷是否發送成功。

RetryExec底層通信使用的是MainClientExec,而MainClientExec底層便調用的是HttpRequestExecutor.doSendRequest()。

http.request_sent參數的設置,是通過HttpRequestExecutor.doSendRequest()方法設置的。

不重試的異常

關于HttpClient重試策略的研究

  1. InterruptedIOException,線程中斷異常
  2. UnknownHostException,找不到對應host
  3. ConnectException,找到了host但是建立連接失敗。
  4. 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類。

子類.png

這就會導致若是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;
    }

http post 方法傳遞參數的2種方式

附錄:

附錄代碼參考,SpringBoot整合HttpClient

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文檔

類RequestConfig 官網API文檔

類HttpClientBuilder 官方API文檔

apache連接池 官方API文檔

httpclient源碼分析之 PoolingHttpClientConnectionManager 獲取連接

2. 相關博客

使用PoolingHttpClientConnectionManager解決友…

HttpClient中post請求http、https示例

Http請求連接池 - HttpClient 連接池

HttpClient 中的三個超時詳解

HttpClient.DefaultRequestHeaders.ExpectContinue。 ExpectContinue的用途是什么,在什么條件下它被設置為true或false。

理解HTTP協議中的 Expect: 100-continue

java.lang.IllegalStateException: Connection pool shut down 的解決方案

httpclient參數配置

高并發場景下的httpClient優化使用

重試機制的分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • 序 這里簡單解釋一下httpclient一些關鍵參數的配置 超時時間 這里設置了socket_timeout以及c...
    go4it閱讀 14,978評論 0 5
  • 6.1 公鑰密鑰加密原理 6.1.1 基礎知識 密鑰:一般就是一個字符串或數字,在加密或者解密時傳遞給加密/解密算...
    AndroidMaster閱讀 4,028評論 1 8
  • 前言 超文本傳輸協議(HTTP)也許是當今互聯網上使用的最重要的協議了。Web服務,有網絡功能的設備和網絡計算的發...
    狂奔的蝸牛_wxc閱讀 5,526評論 0 12
  • 第二章 連接管理 HttpClient有一個對連接初始化和終止,還有在活動連接上I/O操作的完整控制。而連接操作的...
    狂奔的蝸牛_wxc閱讀 1,174評論 0 0
  • 2018年3月24日的今天,姐姐結婚了。 那個和我小時候湊到一起就打架生氣的人,那個失蹤好幾年也沒讓我忘記一絲一毫...
    兮澤Lemon閱讀 359評論 0 0