SpringBoot :SpringBoot 2.x 集成HttpClient

目录

  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优化使用

重试机制的分析



作者:小胖学编程
链接:https://www.jianshu.com/p/e77e9e126f89
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/qq_31432773/article/details/115793460