8af8eac2eda5aa3ed317b53350551da0.png

HttpClient在项目中还是比较常见的,主要都是通过GET或POST请求第三方以获取响应结果。前段时间还了解到也有企业用它来做爬虫。下面我们就从两方面来一起学习HttpClient。

强占着不放

我们先来看一张因未释放连接导致的异常图。

121facd0c63145abe3a5d1204b3dde1b.png

我们再来看代码,代码很简单,用一个static修饰HttpClient的一个对象,也是用static的方式实例化(并发下,static实例化的对象是线程不安全的)。

1@Slf4j

2public class MyHttpClientTest{

3    private static HttpClient client;

4    static {

5        RequestConfig requestConfig = RequestConfig.custom()

6                .setConnectTimeout(5000)

7                .setConnectionRequestTimeout(3000)

8                .setSocketTimeout(5000).build();

9        client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)

10                .build();

11    }

12

13    @Test

14    public void testHttpClient(){

15        for (int i = 0; i 

16            String url = "http://127.0.0.1:9092/learn/product/get/12";

17            HttpGet httpGet = new HttpGet(url);

18            try {

19                HttpResponse res = client.execute(httpGet);

20            } catch (IOException e) {

21                log.error("异常: ",e);

22                return;

23            }

24        }

25    }

26}

我们看回异常图,图中有2个红框,我们先来看第一个红框的“[total kept alive: 0; route allocated: 2 of2; total allocated: 2 of 20]”,这个DEBUG级别日志描述的什么意思呢?也就是说这个route下共有2个连接,已用2个;pool共有20个,已用2个。再来看第二个红框“Timeout waiting for connection from pool”,从连接池获取连接等待超时。奇怪吧,这顺序执行也会出现?这其实是前两个连接响应结果回来后,并没有释放连接资源,导致后面的请求等待超时。

我们看看PoolingHttpClientConnectionManager类的构造函数,其给Pool初始化时给maxConnPerRoute和maxConnTotal设置了默认值。

1    public PoolingHttpClientConnectionManager(

2        final HttpClientConnectionOperator httpClientConnectionOperator,

3        final HttpConnectionFactory connFactory,

4        final long timeToLive, final TimeUnit tunit){

5        super();

6        this.configData = new ConfigData();

7        //defaultMaxPerRoute默认为2,maxTotal默认为20

8        this.pool = new CPool(new InternalConnectionFactory(

9                this.configData, connFactory), 2, 20, timeToLive, tunit);

10        this.pool.setValidateAfterInactivity(2000);

11        this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");

12        this.isShutDown = new AtomicBoolean(false);

13    }

所以当请求连接被占用2个后,后面的请求并不会获取到连接,这么说,我们需要释放连接资源。这里我们需要介绍EntityUtils这个类,里面包含了两个释放连接资源的方法consumeQuietly(HttpEntity entity)和consume(HttpEntity entity),前者内部也是请求后者完成关闭InputStream。调整后的代码如下:

1@Test

2    public void testHttpClient(){

3        for (int i = 0; i 

4            String url = "http://127.0.0.1:9092/learn/product/get/12";

5            HttpGet httpGet = new HttpGet(url);

6            HttpResponse res = null;

7            try {

8                res = client.execute(httpGet);

9            } catch (IOException e) {

10                log.error("异常: ", e);

11                return;

12            } finally {

13                if (null != res) {

14                    EntityUtils.consumeQuietly(res.getEntity());

15                }

16            }

17        }

18    }

调整完代码后,我们执行后发现5个请求都能正确请求并得到响应结果。或许有同学会问到如果我调整maxConnPerRoute和maxConnTotal不也行吗?可以,但是你的连接还是没有释放,当超过你设置值后也会出现无法获得连接池的问题。

被CloseableHttpClient名字所坑

最近项目中就是使用到CloseableHttpClient实现请求第三方,踩完上面的坑后,我们在项目中写了一个工厂类内部用枚举方式实现单例。经过并发测试后,的确没有出现上述坑,看了CloseableHttpClient有一个execute方法,最后也有释放资源,所以没有在意。直到今天写文章对应的测试案例我才发现我被这个方法欺骗了。

CloseableHttpClient的execute()重载了多个方法,而实际调用能释放资源的execute必须是包含ResponseHandler extends T>这个属性,否则和原有HttpClient方式是一样的。为何,模拟高并发下并没有发生问题呢?这是因为项目中请求响应结果后是用下面的代码完成数据获取的。

1 HttpResponse response = client.execute(get);

2 String res = EntityUtils.toString(response.getEntity());

跟进EntityUtils.toString()方法的最底层调用,会执行InputStream关闭流,和上文说的consume方法一样。但是这里坑就坑在,如果请求第三方超时或其他异常,则直接跳过,并没有执行上面的toString()方法,那就不会释放连接资源。

知道坑所在,要解决也就好办。可以有下述两个方案:

1、外层调用增加finally释放连接,上文所述。

2、实例化一个ResponseHandler对象,调用实现了释放连接资源的方法。

总结

今天我们一起学习了HttpClient和CloseableHttpClient没有正确释放连接,以及并发受限的问题。实际项目中如果没有考虑到这两个情况,则会带来生产问题,后面的请求一直无法获取到连接资源,以及并发量起不来。很多项目并不会有这个问题是因为HttpClient和CloseableHttpClient并不是单例的,每次使用都会是新的实例。希望通过今天的学习对你日后使用HttpClient更加得心应手。由于篇幅问题,没有把CloseableHttpClient案例放上来,有兴趣的同学可以到GitHub上下载(https://github.com/littleluo/bj-share-java.git)。

思考和讨论

1、案例中,我们使用了static和枚举的方式解决单例问题,除了这两种方法外,还有哪些方式呢?他们各自的优缺点是什么?

2、说回前面关于分布式锁的文章中,我们用到了redis,也提到client.close()方式归还连接,为何是归还连接,而不是关闭连接呢?

欢迎留言与我分享和指正!也欢迎你把这篇文章分享给你的朋友或同事,一起交流。

感谢您的阅读,我们下节再见!

扫码关注我们,与君共进

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐