雖然生產環境推薦使用 Apache HttpClient、OkHttp 或 JDK 11+ HttpClient,但理解原生 HttpURLConnection 的工作原理是排查網絡問題、面試答題、輕量級項目無依賴發送請求的基礎。本文提供生產級封裝代碼,涵蓋連接池模擬、超時控制、異常處理、資源釋放、響應碼判斷等實戰細節。
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.Map.Entry;
public class HttpRequestUtil {
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
private static final int CONNECT_TIMEOUT = 5000; // 連接超時 5s
private static final int READ_TIMEOUT = 10000; // 讀取超時 10s
/**
* 發送 GET 請求
* @param url 請求地址(不含參數)
* @param params 參數 Map,自動編碼拼接
* @return 響應體字串
*/
public static String sendGet(String url, Map<String, String> params) {
HttpURLConnection conn = null;
BufferedReader reader = null;
StringBuilder response = new StringBuilder();
try {
// 1. 構建完整 URL(含查詢參數)
StringBuilder urlBuilder = new StringBuilder(url);
if (params != null && !params.isEmpty()) {
urlBuilder.append(url.contains("?") ? "&" : "?");
boolean first = true;
for (Entry<String, String> entry : params.entrySet()) {
if (!first) urlBuilder.append("&");
urlBuilder.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
first = false;
}
}
URL requestUrl = new URL(urlBuilder.toString());
// 2. 打開連接並配置
conn = (HttpURLConnection) requestUrl.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setRequestProperty("User-Agent", USER_AGENT);
conn.setRequestProperty("Accept", "application/json, text/plain, */*");
conn.setRequestProperty("Connection", "keep-alive");
// 3. 檢查響應碼
int responseCode = conn.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new HttpException("HTTP 錯誤碼: " + responseCode, responseCode);
}
// 4. 讀取響應
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
} catch (IOException e) {
throw new HttpException("GET 請求異常: " + e.getMessage(), e);
} finally {
// 5. 釋放資源(順序:讀取器 → 連接)
closeQuietly(reader);
if (conn != null) conn.disconnect();
}
return response.toString();
}
/**
* 發送 POST 請求(表單/JSON 通用)
* @param url 請求地址
* @param params 表單參數(application/x-www-form-urlencoded)
* @param jsonBody JSON 字串(application/json),優先級高於 params
* @return 響應體字串
*/
public static String sendPost(String url, Map<String, String> params, String jsonBody) {
HttpURLConnection conn = null;
BufferedReader reader = null;
OutputStreamWriter writer = null;
StringBuilder response = new StringBuilder();
try {
URL requestUrl = new URL(url);
conn = (HttpURLConnection) requestUrl.openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setRequestProperty("User-Agent", USER_AGENT);
conn.setRequestProperty("Accept", "application/json, text/plain, */*");
conn.setRequestProperty("Connection", "keep-alive");
// 關鍵:POST 必須設置輸出/輸入
conn.setDoOutput(true);
conn.setDoInput(true);
// 寫入請求體
if (jsonBody != null && !jsonBody.isEmpty()) {
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
writer = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
writer.write(jsonBody);
} else if (params != null && !params.isEmpty()) {
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
StringBuilder body = new StringBuilder();
boolean first = true;
for (Entry<String, String> entry : params.entrySet()) {
if (!first) body.append("&");
body.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
.append("=")
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
first = false;
}
writer = new OutputStreamWriter(conn.getOutputStream(), "UTF-8");
writer.write(body.toString());
}
if (writer != null) writer.flush();
// 檢查響應碼
int responseCode = conn.getResponseCode();
InputStream inputStream = responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream();
// 讀取響應
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new HttpException("HTTP 錯誤碼: " + responseCode + ", 響應: " + response, responseCode);
}
} catch (IOException e) {
throw new HttpException("POST 請求異常: " + e.getMessage(), e);
} finally {
closeQuietly(writer);
closeQuietly(reader);
if (conn != null) conn.disconnect();
}
return response.toString();
}
// 靜默關閉資源
private static void closeQuietly(Closeable closeable) {
if (closeable != null) {
try { closeable.close(); } catch (IOException ignored) {}
}
}
// 自定義異常攜帶狀態碼
public static class HttpException extends RuntimeException {
private final int statusCode;
public HttpException(String message, int statusCode) { super(message); this.statusCode = statusCode; }
public HttpException(String message, Throwable cause) { super(message, cause); this.statusCode = -1; }
public int getStatusCode() { return statusCode; }
}
}
public class Demo {
public static void main(String[] args) {
// GET 請求
Map<String, String> getParams = new HashMap<>();
getParams.put("key", "123");
getParams.put("v", "456");
try {
String getResult = HttpRequestUtil.sendGet("https://httpbin.org/get", getParams);
System.out.println("GET 響應: " + getResult);
} catch (HttpRequestUtil.HttpException e) {
System.err.println("GET 失敗: " + e.getMessage() + ", 狀態碼: " + e.getStatusCode());
}
// POST 表單
Map<String, String> postParams = new HashMap<>();
postParams.put("username", "test");
postParams.put("password", "123456");
try {
String postResult = HttpRequestUtil.sendPost("https://httpbin.org/post", postParams, null);
System.out.println("POST 表單響應: " + postResult);
} catch (HttpRequestUtil.HttpException e) {
System.err.println("POST 失敗: " + e.getMessage());
}
// POST JSON
String json = "{\"name\":\"張三\",\"age\":28}";
try {
String jsonResult = HttpRequestUtil.sendPost("https://httpbin.org/post", null, json);
System.out.println("POST JSON 響應: " + jsonResult);
} catch (HttpRequestUtil.HttpException e) {
System.err.println("JSON POST 失敗: " + e.getMessage());
}
}
}
| 細節 | 原生代碼做法 | 為什麼重要 |
|---|---|---|
| 超時控制 | setConnectTimeout/setReadTimeout | 防止線程永久阻塞,導致線程池耗盡 |
| 資源釋放順序 | 先關流,後 disconnect() | 避免連接洩漏,確保底層 Socket 歸還 |
| 錯誤流讀取 | responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream() | 4xx/5xx 響應體在 errorStream,不讀會導致連接無法復用 |
| 參數編碼 | URLEncoder.encode(..., "UTF-8") | 特殊字符(空格、中文、&)必須編碼,否則服務端解析錯誤 |
| User-Agent | 設置常見瀏覽器 UA | 部分反爬蟲/網關攔截無 UA 請求 |
| Keep-Alive | Connection: keep-alive | 復用 TCP 連接,減少握手開銷 |
| 重定向處理 | setInstanceFollowRedirects(true) (預設) | 自動跟隨 301/302,或手動處理 Location 標頭 |
setDoOutput(true):POST 請求不設置則無法寫入請求體,拋 ProtocolException。TrustManager / HostnameVerifier,代碼極其繁瑣。HttpURLConnection 底層無連接池,高併發下頻繁創建 Socket,效能遠遜於 HttpClient/OkHttp。<!-- Maven 依賴建議 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.11.0</version>
</dependency>
掌握原生實現,是為了在框架失效時能看懂堆疊追蹤、在受限環境下能快速交付、在面試時能說清 TCP/HTTP 細節。但請記住:生產環境請擁抱成熟客戶端庫。