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. 關鍵實踐原則
- 完全消費原則:若實體內容未完全讀?。ㄈ鐑H讀部分字節),必須關閉響應而非內容流,否則會導致連接僵滯。
- 工具類輔助:
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 |
不可重復(單次讀?。?/td> |
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號