前言
HttpURLConnection 是 JDK 內建的 HTTP 客戶端,零依賴、輕量級、Android 相容。雖然功能不如 Apache HttpClient 豐富,但在簡單場景、受限環境、避免第三方依賴時仍是首選。本文系統性介紹 POST 請求完整流程、參數編碼、超時控制、重導向處理與效能優化。
核心類別關係
URL
│
▼
URLConnection (抽象類)
│
▼
HttpURLConnection ← 我們實際使用的類
│
├── getOutputStream() → 寫入請求體
├── getInputStream() ← 讀取回應體
├── getErrorStream() ← 讀取錯誤回應體 (4xx/5xx)
└── disconnect() ← 釋放連線
標準 POST 流程完整代碼
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class HttpPostClient {
public static String post(String urlStr, Map params, int connectTimeout, int readTimeout)
throws IOException {
// 1. 建立 URL 與連線
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try {
// 2. 核心配置
conn.setRequestMethod("POST");
conn.setDoOutput(true); // 允許寫入請求體
conn.setDoInput(true); // 允許讀取回應體
conn.setUseCaches(false); // 不使用快取
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
conn.setInstanceFollowRedirects(true); // 自動跟隨重導向
// 3. 設定請求 Header
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("User-Agent", "Java/HttpURLConnection");
conn.setRequestProperty("Connection", "Keep-Alive"); // 長連線
// 4. 寫入請求體 (表單參數)
String formData = buildFormData(params);
try (OutputStream os = conn.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
BufferedWriter writer = new BufferedWriter(osw)) {
writer.write(formData);
writer.flush();
}
// 5. 處理回應
int status = conn.getResponseCode();
InputStream is = (status >= 200 && status < 300) ? conn.getInputStream() : conn.getErrorStream();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
} finally {
conn.disconnect(); // 釋放連線回連線池
}
}
/** 建立 application/x-www-form-urlencoded 字串 */
private static String buildFormData(Map params) {
if (params == null || params.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : params.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(encode(entry.getKey())).append('=').append(encode(entry.getValue()));
}
return sb.toString();
}
/** URL 編碼 (RFC 3986) */
private static String encode(String s) {
try {
return URLEncoder.encode(s, StandardCharsets.UTF_8.name())
.replace("+", "%20") // 標準編碼空格為 %20
.replace("*", "%2A")
.replace("~", "%7E");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // 不會發生
}
}
// 測試主程式
public static void main(String[] args) {
try {
Map params = Map.of(
"username", "test_user",
"password", "123456",
"remember", "true"
);
String result = post("https://localhost:8080/api/login", params, 5000, 10000);
System.out.println("回應: " + result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
關鍵技術細節解析
1. 請求體寫入機制
setDoOutput(true) 必須在 getOutputStream() 前呼叫
- 預設
Content-Type 為 application/x-www-form-urlencoded
- 輸出流關閉前資料不會真正發送 (緩衝機制)
2. 超時設定雙重保險
| 方法 | 含義 | 建議值 |
setConnectTimeout(ms) | TCP 三向交握建立連線最長等待 | 3000-5000ms |
setReadTimeout(ms) | 從伺服器讀取資料間隔最長等待 | 10000-30000ms |
⚠️ 必須兩者都設,否則預設為無限等待,導致線程阻塞耗盡。
3. 重導向處理
setInstanceFollowRedirects(true):單一連線層級啟用
HttpURLConnection.setFollowRedirects(true):全域靜態啟用 (影響所有連線)
- POST 重導向預設行為:HTTP 1.1 規範要求 301/302 重導向時保持 POST 方法,但瀏覽器常改為 GET,Java 預設遵循規範保持 POST
4. 連線複用與 Keep-Alive
// 啟用 Keep-Alive (HTTP/1.1 預設開啟)
conn.setRequestProperty("Connection", "Keep-Alive");
// JDK 內建連線池 (透過 sun.net.www.http.HttpClient 控制)
// 系統屬性調整 (啟動時設定 -D 或 System.setProperty)
System.setProperty("http.keepAlive", "true");
System.setProperty("http.maxConnections", "50"); // 每個目的地最大連線數
進階場景:JSON POST / 檔案上傳
發送 JSON
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
String json = "{\"name\":\"test\",\"age\":25}";
try (OutputStream os = conn.getOutputStream()) {
os.write(json.getBytes(StandardCharsets.UTF_8));
}
Multipart 檔案上傳 (手動構建)
String boundary = "----Boundary" + System.currentTimeMillis();
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (OutputStream os = conn.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8), true)) {
// 文字欄位
writer.append("--").append(boundary).append("\r\n");
writer.append("Content-Disposition: form-data; name=\"username\"\r\n\r\n");
writer.append("test_user").append("\r\n");
// 檔案欄位
writer.append("--").append(boundary).append("\r\n");
writer.append("Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n");
writer.append("Content-Type: text/plain\r\n\r\n");
writer.flush();
Files.copy(Paths.get("test.txt"), os);
os.write("\r\n".getBytes());
// 結束邊界
writer.append("--").append(boundary).append("--\r\n");
writer.flush();
}
效能優化與最佳實務
- 複用 URLConnection:同一
URL 物件可多次 openConnection(),底層共享連線池
- 關閉連線:務必在
finally 呼叫 disconnect(),否則連線洩漏
- 串流處理大回應:避免
EntityUtils.toString() 式一次性讀入,改用 BufferedReader 逐行或串流處理
- 錯誤流處理:4xx/5xx 狀態碼需讀取
getErrorStream() 獲取錯誤詳情
- 編碼一致性:請求/回應/URL 編碼統一使用 UTF-8
常見錯誤排查表
| 現象 | 可能原因 | 解決方案 |
| 連線超時 | 防火牆、IP錯誤、埠未開放 | telnet 測試、檢查 security group |
| 讀取超時 | 後端處理過慢、死鎖 | 調大 readTimeout、優化後端 |
| 400 Bad Request | 參數格式錯誤、編碼不符 | 抓包對照、檢查 Content-Type |
| 中文亂碼 | 編碼不一致 | 全鏈路 UTF-8、URLEncoder.encode |
| 連線洩漏 | 未呼叫 disconnect() | finally 區塊強制釋放 |
總結
HttpURLConnection 雖 API 較低階,但掌握其請求/回應流、超時控制、編碼處理、連線釋放核心機制,即可應對 80% 的 HTTP 整合場景。若需連線池監控、自動重試、攔截器、OAuth 等進階功能,再升級 Apache HttpClient 或 OkHttp。
- 上一篇文章: Java super 關鍵字深度解析:父類成員存取、建構子鏈與多型場景實戰
- 下一篇文章: Java Web Session 登入驗證完整實作:從登入攔截到登出清理的安全最佳實務
Copyright ©
程式碼之家 All Rights Reserved