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 专业解释

  1. 请求执行流程
  • 连接管理:会从连接池里获取或者新建一个连接,并且要保证连接的有效性。
  • 请求准备:把 HTTP 请求转换为可以传输的格式,同时添加必要的头部信息,像 User-Agent、Content-Type 等。
  • 传输处理:借助底层的 Socket 进行数据传输,在这个过程中会处理诸如超时、重定向、认证等情况。
  • 响应解析:对服务器返回的数据进行解析,生成 HttpResponse 对象。
  • 连接释放:根据 Keep-Alive 策略来决定是关闭连接还是将其返回到连接池。
  1. 核心组件
  • 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();
        }
    }
}

执行流程说明:

    1. 创建客户端:构建一个默认配置的 HttpClient 实例,就像开了一家快递公司
    1. 创建请求:创建一个 HttpGet 对象,设置目标 URL,这类似于准备好要寄送的包裹并写上地址。
    1. 执行请求:调用 httpClient.execute() 方法,这相当于把包裹交给快递公司去寄送。
    1. 处理响应:获取并解析服务器返回的状态码和内容,这就如同收到了包裹送达的反馈信息。
    1. 释放资源:关闭响应和客户端,类似于完成一次快递业务后,清理相关资源。

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 将实体分为三类:

  1. 流式 (Streamed):内容来自流(如网络响应),通常不可重复读取。
  2. 自包含 (Self-contained):内容在内存中(如 StringEntity, ByteArrayEntity),可重复读取。
  3. 包装 (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); 

优势

  1. handleResponse 内完成状态校验→内容解析→资源转换全流程
  2. 异常时自动抛出 HttpResponseException 保证连接释放

核心总结

  1. 执行流程

    graph LR A[创建请求对象] --> B[HttpClient.execute] B --> C{成功?} C -->|是| D[处理响应实体] C -->|否| E[处理异常] D --> F[确保关闭资源]
  2. 关键实践

    • 资源释放:始终用 try-with-resourcesfinally 块关闭响应/流。
    • 实体选择
      • 可重复读取 → 用 ByteArrayEntity/StringEntity
      • 大文件 → 用 FileEntity
    • 表单提交:用 UrlEncodedFormEntity 自动编码参数。
    • 响应处理:优先用 ResponseHandler 简化资源管理。
  3. 性能注意

    • 避免用 EntityUtils.toString() 处理大响应。
    • 流式实体 (InputStreamEntity) 不可重复,需谨慎使用。
posted @ 2025-07-18 18:04  hqq的进阶日记  阅读(39)  评论(0)    收藏  举报