1. 概述

Apache HttpClient 是一个流行的 Java 库,它提供了高效且功能丰富的包,实现了最新HTTP标准的客户端部分。该库旨在易于扩展,同时为基本的HTTP方法提供了强大的支持。

在这篇教程中,我们将探讨Apache HttpClient API的设计。我们将解释HttpClientCloseableHttpClient之间的区别,并了解如何使用HttpClientsHttpClientBuilder创建CloseableHttpClient实例。

最后,我们将推荐在自定义代码中应使用哪种API,并查看哪些API类实现了Closeable接口,因此需要关闭它们的实例以释放资源。

2. API设计

让我们首先了解API的设计,重点关注其高层类和接口。下图展示了执行经典HTTP请求和处理HTTP响应所需的API部分:

此外,Apache HttpClient API还支持异步HTTP请求/响应交换,以及使用RxJava进行反应式消息交换。

3. HttpClient vs. CloseableHttpClient

HttpClient是一个代表HTTP请求执行基本合同的高层接口。它对请求执行过程没有任何限制,而状态管理、身份验证和重定向等具体细节留给各个客户端实现。

我们可以将任何客户端实现转换为HttpClient接口,从而通过默认客户端实现执行基本HTTP请求:

HttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(serviceOneUrl);
httpClient.execute(httpGet, response -> {
      assertThat(response.getCode()).isEqualTo(HttpStatus.SC_OK);
      return response;
});

请注意execute方法的第二个参数是一个HttpClientResponseHandler函数式接口。

然而,上述代码会在SonarQube上产生一个阻塞问题。原因在于默认客户端实现返回的是一个CloseableHttpClient实例,这需要关闭。

CloseableHttpClient是一个抽象类,代表**HttpClient接口的基本实现**。但它也实现了Closeable接口,因此在使用后应关闭所有实例。我们可以通过使用try-with-resources或在finally块中调用close方法来关闭它们:

try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
     HttpGet httpGet = new HttpGet(serviceOneUrl);
     httpClient.execute(httpGet, response -> {
       assertThat(response.getCode()).isEqualTo(HttpStatus.SC_OK);
       return response;
     });
}

因此,在我们的自定义代码中,应该使用CloseableHttpClient类,而不是HttpClient接口。

4. HttpClients vs. HttpClientBuilder

在上面的例子中,我们使用了HttpClients类的一个静态方法来获取默认的客户端实现。HttpClients是一个包含用于创建CloseableHttpClient实例的工厂方法的工具类

CloseableHttpClient httpClient = HttpClients.createDefault();

我们可以使用HttpClientBuilder类实现相同的功能。HttpClientBuilder用于创建CloseableHttpClient实例的构建者模式实现

CloseableHttpClient httpClient = HttpClientBuilder.create().build();

内部地,HttpClients使用HttpClientBuilder来创建客户端实现实例。因此,在自定义代码中,我们应该优先使用HttpClients。鉴于它是更高层次的类,其内部可能会随着新版本的发布而变化。

5. 资源管理

我们需要关闭CloseableHttpClient实例的原因是在其作用域之外关闭关联的连接管理器。

5.1. 自动资源释放(HttpClient 4.x)

在当前的Apache HttpClient 5版本中,通过我们之前看到的HttpClientResponseHandler,通信结束后会自动释放客户端资源。

在当前版本之前,为了向HttpClient 4.x提供向后兼容性,提供了CloseableHttpResponse

CloseableHttpResponse是一个实现ClassicHttpResponse接口的类。然而,ClassicHttpResponse还扩展了HttpResponseHttpEntityContainerCloseable接口。

HTTP连接由响应对象持有,以便可以直接从网络套接字流式传输响应内容。因此,在自定义代码中,我们应该使用CloseableHttpResponse类而不是HttpResponse接口。一旦消费完响应,我们也需要确保调用close方法:

try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
    HttpGet httpGet = new HttpGet(serviceUrl);
    try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
        HttpEntity entity = response.getEntity();
        EntityUtils.consume(entity);
    }
}

值得注意的是,当响应内容未完全消耗时,底层连接不能安全地重新使用。在这种情况下,连接将被连接管理器关闭并丢弃。

5.2. 重用客户端

每次请求都关闭一个CloseableHttpClient实例然后再创建新的可能是一个昂贵的操作。相反,我们可以使用单个CloseableHttpClient实例发送多个请求,以避免频繁关闭和创建连接管理器

try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
      HttpGet httpGetOne = new HttpGet(serviceOneUrl);
      httpClient.execute(httpGetOne, responseOne -> {
          HttpEntity entityOne = responseOne.getEntity();
          EntityUtils.consume(entityOne);
          assertThat(responseOne.getCode()).isEqualTo(HttpStatus.SC_OK);
          return responseOne;
      });

      HttpGet httpGetTwo = new HttpGet(serviceTwoUrl);
      httpClient.execute(httpGetTwo, responseTwo -> {
           HttpEntity entityTwo = httpGetTwo.getEntity();
           EntityUtils.consume(entityTwo);
           assertThat(responseTwo.getCode()).isEqualTo(HttpStatus.SC_OK);
           return responseTwo;
      });
}

这样,我们就避免了关闭内部关联的连接管理器并创建一个新的。

6. 总结

在这篇文章中,我们深入探讨了Apache HttpClient的经典HTTP API,这是一个流行的Java客户端HTTP库。

我们学习了HttpClientCloseableHttpClient的区别,并建议在自定义代码中使用CloseableHttpClient。接下来,我们了解了如何使用HttpClientsHttpClientBuilder创建CloseableHttpClient实例。

最后,我们关注了CloseableHttpClientCloseableHttpResponse类,它们都实现了Closeable接口。我们了解到,这些实例的关闭对于释放资源至关重要。

如往常一样,完整的源代码可以在GitHub上找到。对于HttpClient 4.x的代码片段,请参阅我们的HttpClient 4模块