httpclient5 通用HTTP客户端

在Java开发中我们经常需要使用HTTP客户端库发起HTTP请求,常见用途包括客户端请求接口、爬虫搜集网页信息、实现RPC框架等。Apache HttpClient是Java中的一个老牌HTTP客户端库,如今最新版本httpclient5是Apache HttpComponents项目下的一个子模块。实际上Apache HttpClient有3、4、5三个版本,3在老项目中可能很常见,但该版本已经不再维护更新,不推荐使用。4和5的用法类似,主要是包名发生了变化,建议使用最新版本。

官方主页:https://hc.apache.org/index.html

同类技术比较

Java生态中,HTTP客户端主要有三种实现类库:

  1. 使用JDK自带的HttpURLConnection
  2. Apache HttClient(常用)
  3. OkHttpClient(常用)

HttpURLConnection由于封装度低,因此也是最难使用的,其唯一的优势就是JDK内置,不必引入额外依赖,不过还是一般都不会考虑;OkHttpClient是随着Android开发而流行起来的一个库,它功能强大,而且API设计非常优雅;Apache的HttClient则是一个比较老牌的HTTP客户端库,在传统企业级项目中应用比较广泛。

除了以上三种方式,我们也可以基于Socket封装HTTP客户端或是基于Netty客户端封装,以尽可能的符合我们的使用场景。HTTP客户端的实现方式有很多,不过Apache HttpClient和OkHttpClient是使用人数最多的两个选择。

引入Maven依赖

pom.xml中,如下配置添加httpclient5依赖。

<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.1.3</version>
</dependency>

此外我们还需要添加日志相关依赖。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

注意httpclient5依赖SLF4J日志API,因此工程中应当包含SLF4J及日志实现框架,如Logback等。如果不引入日志库,虽然程序逻辑也能正常运行,但不会有任何日志输出,不利于我们调试代码。

HttpClient使用例子

这里我们介绍httpclient5最基本的使用方式。

发起GET请求

下面我们简单看一个例子,例子中我们使用httpclient5向服务器发起GET请求。

package com.gacfox.demo;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.URIBuilder;

@Slf4j
public class Main {
    public static void main(String[] args) {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            List<NameValuePair> nvps = new ArrayList<>();
            nvps.add(new BasicNameValuePair("userId", "1"));
            URI uri = new URIBuilder()
                    .setScheme("http")
                    .setHost("localhost")
                    .setPort(8080)
                    .setPath("/api/v1/getUserById")
                    .addParameters(nvps)
                    .build();

            HttpGet httpGet = new HttpGet(uri);

            try (CloseableHttpResponse response = client.execute(httpGet)) {
                if (response.getCode() == HttpStatus.SC_OK) {
                    String resp = EntityUtils.toString(response.getEntity(), "UTF-8");
                    log.info("resp: {}", resp);
                } else {
                    log.info("response code: {}", response.getCode());
                }
            }
        } catch (URISyntaxException | IOException | ParseException e) {
            log.error("Exception occurred during HTTP GET request", e);
        }
    }
}

代码非常简单,我们用httpclient5提供的URIBuilder工具类构造了一个请求URI,包括请求主机、端口、路径、参数信息,然后通过调用相关方法,获得了响应内容。

注意这里我们由于只是写了个只执行一次的例子,因此将CloseableHttpClient用完后就关闭了。实际上CloseableHttpClient是线程安全的,它内部维护了一个连接池,可以在多线程环境下共享使用,我们可以在程序退出时再关闭CloseableHttpClient

发起POST请求

下面例子向服务器发起了POST请求,写法和GET请求类似,只不过请求内容的组装有些区别。

package com.gacfox.demo;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.URIBuilder;

@Slf4j
public class Main {
    public static void main(String[] args) {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            URI uri = new URIBuilder()
                    .setScheme("http")
                    .setHost("localhost")
                    .setPort(8080)
                    .setPath("/api/v1/saveUser")
                    .build();

            HttpPost httpPost = new HttpPost(uri);
            httpPost.setHeader("Content-Type", "application/json");
            HttpEntity httpEntity = new StringEntity("{\"username\":\"tom\", \"password\":\"abc123\"}");
            httpPost.setEntity(httpEntity);

            try (CloseableHttpResponse response = client.execute(httpPost)) {
                if (response.getCode() == HttpStatus.SC_OK) {
                    String resp = EntityUtils.toString(response.getEntity(), "UTF-8");
                    log.info("resp: {}", resp);
                } else {
                    log.info("response code: {}", response.getCode());
                }
            }

        } catch (URISyntaxException | IOException | ParseException e) {
            log.error("Exception occurred during HTTP POST request", e);
        }
    }
}

POST请求和GET请求的主要区别就是带有请求体,这里我们的请求体为一个JSON字符串。代码中,简单起见我们直接硬编码了一个JSON字符串,注意这里我们还设置了请求头Content-Typeapplication/json

关于close方法的说明

上面例子代码中,我们对于CloseableHttpClientCloseableHttpResponse都是使用了try-with-resources语法用完立刻关闭了的,实际开发中我们需要根据实际情况做出一些调整。

CloseableHttpClient是一个线程安全的对象,其内部维护了连接池,我们可以在多线程环境中对其使用单例模式,如果closeableHttpClient.close()方法被调用,这个对象相关的资源就会被释放,需要重新创建才能再次使用。

CloseableHttpResponse对象则是针对一次HTTP连接生成的,请求返回我们使用后需要将其关闭。不过实际上closeableHttpResponse.close()会在读取响应体时自动调用,当然我们再手动调用一次更加可靠。

请求超时设置

如果我们开发一个偏底层的HTTP请求库,一定要记得给请求设置超时时间。考虑这样的例子:我们编写了一个HTTP请求框架用于服务间接口调用,如果上游系统已经不堪重负导致运行缓慢,且调用端没有设置超时时间,大量请求就会堆积在调用端服务器上无法释放,引起整个系统的故障。httpclient5的默认超时时间为3分钟,如果用于RPC框架就非常恐怖了。

因此,最佳实践是任何时候都应该给HTTP客户端设置超时,对并发性能要求更高的系统,也要求更短的超时时间。

RequestConfig requestConfig = RequestConfig.custom()
        // 从连接池中获取连接的超时时间
        .setConnectionRequestTimeout(5000, TimeUnit.MILLISECONDS)
        // 建立TCP连接的超时时间
        .setConnectTimeout(5000, TimeUnit.MILLISECONDS)
        // 获取响应的超时时间
        .setResponseTimeout(5000, TimeUnit.MILLISECONDS).build();

// 获取HttpClient对象
CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();

上面代码中,我们创建了一个RequestConfig对象,该对象能够配置CloseableHttpClient的三项超时相关的参数。如果请求达到超时时间,就会抛出异常java.net.SocketTimeoutException: Read timed out

连接池设置

前面说过CloseableHttpClient内部维护了连接池,实际上我们也有必要手动进行一些参数设置。

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
// 连接池最大连接数
connectionManager.setMaxTotal(1000);
// 每route最大连接数
connectionManager.setDefaultMaxPerRoute(32);

// 获取HttpClient对象
CloseableHttpClient client = HttpClients.custom().setConnectionManager(connectionManager).build();

连接池中主要有两个参数可以设置:

  • maxTotal:连接池最大连接数,默认为25
  • defaultMaxPerRoute:每route最大连接数,默认为5。我们知道HTTP1.1中可以保持长连接使得多个HTTP请求复用一个TCP连接来减少开销,这个参数就是指一个空闲长连接上的最大请求数
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。