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客户端主要有三种实现类库:
- 使用JDK自带的
HttpURLConnection类 - Apache HttClient(常用)
- 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-Type为application/json。
关于close方法的说明
上面例子代码中,我们对于CloseableHttpClient和CloseableHttpResponse都是使用了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:连接池最大连接数,默认为25defaultMaxPerRoute:每route最大连接数,默认为5。我们知道HTTP1.1中可以保持长连接使得多个HTTP请求复用一个TCP连接来减少开销,这个参数就是指一个空闲长连接上的最大请求数