Apache HttpClient 4.5.x 学习总结二:Request execution
请求执行(Request execution)
1.什么是请求执行
-
- Request execution 是指从发送HTTP请求到获取响应整个过程的实现机制
HttpClient 的核心功能是执行 HTTP 方法。HTTP 方法的执行涉及一次或多次 HTTP 请求/响应交换,通常由 HttpClient 内部处理。用户需提供要执行的请求对象,HttpClient 负责将请求发送至目标服务器,返回对应的响应对象;若执行失败,则抛出异常。
1.1 通俗解释
可以把 Request execution 想象成一次快递寄送过程:
- 快递公司(CloseableHttpClient):就像是一家快递公司,提供了各种寄送服务,比如标准快递、加急快递等。
- 包裹处理(请求准备):你把要寄送的物品(请求数据)打包好,写上收件人地址(URL)和寄件人信息(头部信息)。
- 运输过程(传输处理):快递公司安排货车或者飞机(连接)来运输包裹。在运输途中,可能会遇到堵车(超时)、需要绕行(重定向)等情况。
- 送达签收(响应解析):包裹送达后,收件人会签收(返回响应),然后信息再反馈给你(解析响应数据)。
- 车辆调度(连接管理):快递公司不会每次送完一个包裹就把车扔掉,而是会根据情况决定是让车继续跑下一趟(Keep-Alive),还是入库备用(连接池)。
1.2 专业解释
- 请求执行流程
- 连接管理:会从连接池里获取或者新建一个连接,并且要保证连接的有效性。
- 请求准备:把 HTTP 请求转换为可以传输的格式,同时添加必要的头部信息,像 User-Agent、Content-Type 等。
- 传输处理:借助底层的 Socket 进行数据传输,在这个过程中会处理诸如超时、重定向、认证等情况。
- 响应解析:对服务器返回的数据进行解析,生成 HttpResponse 对象。
- 连接释放:根据 Keep-Alive 策略来决定是关闭连接还是将其返回到连接池。
- 核心组件
- CloseableHttpClient:这是执行请求的主入口,负责管理连接和执行策略。
- HttpRequestExecutor:负责处理请求的发送和响应的接收。
- HttpClientContext:保存请求执行过程中的上下文信息,例如认证状态、重定向历史等。
- ConnectionManager:对底层的连接资源进行管理。
1.3 举例和执行流程说明
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class HttpClientExample {
public static void main(String[] args) throws Exception {
// 创建快递公司(CloseableHttpClient)
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
// 创建GET请求(包裹)
HttpGet httpGet = new HttpGet("https://www.example.com");
// 执行请求(寄送包裹)
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
// 处理响应(签收包裹)
System.out.println("Status Code: " + response.getStatusLine().getStatusCode());
System.out.println("Response Body: " + EntityUtils.toString(response.getEntity()));
} finally {
// 释放资源
response.close();
}
} finally {
// 关闭客户端(关闭快递公司)
httpClient.close();
}
}
}
执行流程说明:
-
- 创建客户端:构建一个默认配置的 HttpClient 实例,就像开了一家快递公司
-
- 创建请求:创建一个 HttpGet 对象,设置目标 URL,这类似于准备好要寄送的包裹并写上地址。
-
- 执行请求:调用 httpClient.execute() 方法,这相当于把包裹交给快递公司去寄送。
-
- 处理响应:获取并解析服务器返回的状态码和内容,这就如同收到了包裹送达的反馈信息。
-
- 释放资源:关闭响应和客户端,类似于完成一次快递业务后,清理相关资源。
2 HTTP 请求 (HTTP request)
所有 HTTP 请求都包含一个由方法名、请求 URI 和 HTTP 协议版本组成的请求行。
HttpClient 原生支持 HTTP/1.1 规范定义的所有方法:
GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS。每种方法有对应的类:HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTrace, HttpOptions。
请求 URI 是标识请求资源的统一资源标识符,包含协议方案、主机名、端口(可选)、资源路径、查询参数(可选)和片段(可选)。
代码示例
// 直接构建 URI
HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en&q=httpclient");
// 使用 URIBuilder 构建(推荐)
URI uri = new URIBuilder()
.setScheme("http")
.setHost("www.google.com")
.setPath("/search")
.addParameter("q", "httpclient") // 自动编码参数
.build();
HttpGet httpget = new HttpGet(uri);
解读
- 请求组成:方法 + URI + 协议版本。
- 方法支持:覆盖 HTTP/1.1 全部方法,每方法有专属类。
- URI 构建:推荐使用
URIBuilder自动处理编码和结构。
3 HTTP 响应 (HTTP response)
HTTP 响应是服务器接收并解析请求后返回的消息。首行包含协议版本、数字状态码和状态文本。
代码示例
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 200, "OK");
System.out.println(response.getStatusLine()); // 输出: "HTTP/1.1 200 OK"
解读
- 响应结构:协议版本 + 状态码 + 状态文本(如
HTTP/1.1 200 OK)。 - 关键信息:通过
StatusLine对象获取状态详情。
4 消息头处理 (Working with message headers)
HTTP 消息可包含多个描述消息属性的头部(如
Content-Length,Content-Type)。HttpClient 提供检索、添加、删除和枚举头部的方法。
代码示例
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
// 获取首个/最后一个指定头部
Header firstHeader = response.getFirstHeader("Set-Cookie");
// 遍历所有 Set-Cookie 头部(高效方式)
HeaderIterator it = response.headerIterator("Set-Cookie");
while (it.hasNext()) {
System.out.println(it.next());
}
// 解析头部值(如拆解 Cookie 的键值对)
HeaderElementIterator elemIt = new BasicHeaderElementIterator(response.headerIterator("Set-Cookie"));
while (elemIt.hasNext()) {
HeaderElement elem = elemIt.nextElement();
System.out.println(elem.getName() + " = " + elem.getValue());
for (NameValuePair param : elem.getParameters()) {
System.out.println(" " + param);
}
}
解读
- 头部操作:支持增删改查和迭代。
- 高效遍历:
HeaderIterator避免创建临时数组。 - 复杂解析:
HeaderElementIterator可拆解含多个键值对的头部(如Set-Cookie)。
5 HTTP 实体 (HTTP entity)
HTTP 消息可携带与请求/响应关联的内容实体。实体在请求(如 POST/PUT)和响应中是可选的。HttpClient 将实体分为三类:
- 流式 (Streamed):内容来自流(如网络响应),通常不可重复读取。
- 自包含 (Self-contained):内容在内存中(如
StringEntity,ByteArrayEntity),可重复读取。- 包装 (Wrapping):内容来自其他实体。
关键点
- 可重复性:仅自包含实体(如
ByteArrayEntity)支持多次读取。 - 字符编码:字符类实体(如文本)需指定编码(如 UTF-8)。
代码示例
// 创建带编码的文本实体
StringEntity entity = new StringEntity("重要消息", ContentType.create("text/plain", "UTF-8"));
System.out.println(entity.getContentType()); // Content-Type: text/plain; charset=utf-8
System.out.println(EntityUtils.toString(entity)); // "重要消息"
6 确保释放底层资源 (Ensuring release of resources)
为确保系统资源正确释放,必须关闭实体关联的内容流或响应对象本身。
代码示例
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
try {
// 执行有效操作
} finally {
instream.close(); // 关闭内容流
}
}
} finally {
response.close(); // 关闭响应
}
关键区别
- 关闭内容流(instream.close())会尝试消费实体内容以保持底层连接存活;
- 关闭响应(response.close())会立即终止并丢弃连接。
6.1. 资源释放的核心逻辑
- 双重保险机制:通过
try-finally嵌套确保:- 内容流(
instream)优先在内部finally关闭 - 响应(
response)在外部finally兜底关闭
- 内容流(
- 本质目标:防止网络连接、内存等资源泄漏,尤其在高并发场景下。
6.2. 关闭内容流 vs 关闭响应的本质区别
| 操作 | 连接状态 | 适用场景 |
|---|---|---|
instream.close() |
保持活跃可复用 | 需复用连接的高性能场景 |
response.close() |
立即终止丢弃 | 无需复用或部分读取的优化场景 |
6.3. 关键实践原则
- 完全消费原则:若实体内容未完全读取(如仅读部分字节),必须关闭响应而非内容流,否则会导致连接僵滞。
- 工具类辅助:
EntityUtils.consume(entity)封装了内容消费与流关闭逻辑,简化代码。 - 写入资源释放:
writeTo()方法内部若调用getContent(),同样需显式关闭流。
6.4. 性能取舍场景
当仅需读取响应头或少量数据时(如检查文件头),强制消费整个实体(以复用连接)可能得不偿失。此时直接关闭响应牺牲连接复用性,换取更高吞吐效率。
6.5. 资源安全层次
结论:关闭响应(response.close())会级联释放所有关联资源,是最彻底的清理方式;而关闭流仅释放内存/文件资源,不影响连接状态。
7 消费实体内容 (Consuming entity content)
推荐使用
HttpEntity.getContent()或writeTo(OutputStream)消费内容。EntityUtils提供便捷方法(如toString(entity)),但除非响应实体来自可信HTTP服务器且长度有限,否则强烈不建议使用 EntityUtils。
安全消费示例
try (CloseableHttpResponse response = httpclient.execute(httpget)) {
HttpEntity entity = response.getEntity();
if (entity != null) {
long len = entity.getContentLength();
if (len != -1 && len < 2048) {
System.out.println(EntityUtils.toString(entity)); // 小内容直接转换
} else {
try (InputStream instream = entity.getContent()) {
// 流式处理大内容
}
}
}
}
多次重复读取实体
若需重复读取实体内容,必须通过内存或磁盘进行缓冲,最简单的方式市使用
BufferedHttpEntity包装原始实体,其会将内容读入内存缓冲区,同时保留原始实体中的所有属性。
HttpEntity entity = response.getEntity();
if (entity != null && !entity.isRepeatable()) {
entity = new BufferedHttpEntity(entity); // 缓冲到内存实现重复读取
}
8 生成实体内容 (Producing entity content)
HttpClient 作为常见数据容器,提供多种实体类封装内容:
StringEntity:字符串ByteArrayEntity:字节数组InputStreamEntity:输入流(不可重复)FileEntity:文件
代码示例
// 发送文件
FileEntity entity = new FileEntity(new File("data.txt"), ContentType.TEXT_PLAIN);
HttpPost httppost = new HttpPost("http://host/upload");
httppost.setEntity(entity); //注入实体
实体内容生成策略:
| 数据类型 | 专用类 | 关键特性 |
|---|---|---|
| 文本 | StringEntity |
内存高效 |
| 二进制数据 | ByteArrayEntity |
可重复读取 |
| 文件 | FileEntity |
支持大文件流式传输 |
| 动态流 | InputStreamEntity |
不可重复(单次读取) |
8.1 HTML 表单
使用
UrlEncodedFormEntity模拟表单提交。
代码示例
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("username", "admin"));
params.add(new BasicNameValuePair("password", "123456"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "UTF-8");
HttpPost httppost = new HttpPost("http://host/login");
httppost.setEntity(entity); // 内容: username=admin&password=123456
此实体将生成URL编码内容:username=admin&password=123456
- 编码机制:UrlEncodedFormEntity 自动将参数转换为 x-www-form-urlencoded 格式
- 关键陷阱:
- 未显式指定字符集(如 Consts.UTF_8)可能导致中文乱码
- 参数值需手动进行 URL 安全编码(URLEncoder.encode())
8.2 分块传输 (Content chunking)
启用分块编码:
StringEntity entity = new StringEntity("大数据", ContentType.TEXT_PLAIN); entity.setChunked(true); // 启用分块传输提示注意:仅当服务器支持(如 HTTP/1.1)时生效。
- 优势:无需预知总长度,支持动态生成内容
- 限制: HTTP/1.0 不支持,此时忽略该标识
9 响应处理器 (Response handlers)
最简洁的响应处理方式是使用
ResponseHandler接口的handleResponse(HttpResponse response)方法。该方法自动管理连接释放,无论请求成功或异常。
代码示例
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
// 定义响应处理器
ResponseHandler<MyJsonObject> rh = response -> {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
// 状态码异常处理
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
if (entity == null) {
throw new ClientProtocolException("Response contains no content");
}
// 按字符集解析JSON
ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset();
Reader reader = new InputStreamReader(entity.getContent(), charset);
return new Gson().fromJson(reader, MyJsonObject.class); // 自动关闭流
};
// 执行请求并自动处理连接释放
MyJsonObject myjson = httpclient.execute(httpget, rh);
优势:
- 在
handleResponse内完成状态校验→内容解析→资源转换全流程 - 异常时自动抛出
HttpResponseException保证连接释放
核心总结
-
执行流程:
graph LR A[创建请求对象] --> B[HttpClient.execute] B --> C{成功?} C -->|是| D[处理响应实体] C -->|否| E[处理异常] D --> F[确保关闭资源] -
关键实践:
- 资源释放:始终用
try-with-resources或finally块关闭响应/流。 - 实体选择:
- 可重复读取 → 用
ByteArrayEntity/StringEntity。 - 大文件 → 用
FileEntity。
- 可重复读取 → 用
- 表单提交:用
UrlEncodedFormEntity自动编码参数。 - 响应处理:优先用
ResponseHandler简化资源管理。
- 资源释放:始终用
-
性能注意:
- 避免用
EntityUtils.toString()处理大响应。 - 流式实体 (
InputStreamEntity) 不可重复,需谨慎使用。
- 避免用
浙公网安备 33010602011771号