SpringBoot2.x集成HttpClient

JAVA && Spring && SpringBoot2.x — 學(xué)習(xí)目錄

目錄

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

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

使用HttpClient發(fā)送請求和接收響應(yīng)的步驟:

  1. 創(chuàng)建CloseableHttpClient對象;
  2. 創(chuàng)建請求方法實例,并指定請求URL。例:如果要發(fā)送Get請求,創(chuàng)建HttpGet對象;如果要發(fā)送POST請求,創(chuàng)建HttpPost對象;
  3. 如果需要發(fā)送參數(shù),則調(diào)用setEntity(HttpEntity entity)方法來設(shè)置參數(shù);
  4. 調(diào)用HttpGet/HttpPost對象的setHeader(String name,String value)方法設(shè)置header信息,或者調(diào)用setHeader(Header[] headers)設(shè)置一組header參數(shù);
  5. 調(diào)用CloseableHttpClient對象的execute(HttpUriRequest request)發(fā)送請求,該方法返回一個CloseableHttpResponse;
  6. 調(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)頭;
  7. 釋放連接。無論執(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)信息截圖。

并發(fā)響應(yīng)截圖.png

可以看到,只有5筆請求并發(fā)的調(diào)用遠程服務(wù)端,得到響應(yīng)之后。再次有5筆請求調(diào)用服務(wù)端。

  1. MaxtTotal是整個池子的大??;
  2. DefaultMaxPerRoute是根據(jù)連接到的主機對MaxTotal的一個細分;比如:
    MaxtTotal=400 DefaultMaxPerRoute=200
    而我只連接到http://sishuok.com時,到這個主機的并發(fā)最多只有200;而不是400;
    而我連接到http://sishuok.comhttp://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)大一些DefaultMaxPerRouteMaxTotal的大小。

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

  1. 如果重試超過3次,則不進行重試;
  2. 如果重試是特殊異常及其子類,則不重試(見下文);
  3. 同一個請求在異步任務(wù)被終止,則不請求;
  4. 冪等的方法可以進行重試,比如Get;
  5. 如果請求未被發(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è)置的。

不重試的異常

關(guān)于HttpClient重試策略的研究

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

子類.png

這就會導(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;
    }

http post 方法傳遞參數(shù)的2種方式

附錄:

附錄代碼參考,SpringBoot整合HttpClient

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

類RequestConfig 官網(wǎng)API文檔

類HttpClientBuilder 官方API文檔

apache連接池 官方API文檔

httpclient源碼分析之 PoolingHttpClientConnectionManager 獲取連接

2. 相關(guān)博客

使用PoolingHttpClientConnectionManager解決友…

HttpClient中post請求http、https示例

Http請求連接池 - HttpClient 連接池

HttpClient 中的三個超時詳解

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

理解HTTP協(xié)議中的 Expect: 100-continue

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

httpclient參數(shù)配置

高并發(fā)場景下的httpClient優(yōu)化使用

重試機制的分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容