Java 發送 HTTP 請求有三大主流方案:JDK 內建 HttpURLConnection (舊)、JDK 11+ HttpClient (現代標準)、OkHttp (業界事實標準)。本文提供統一介面 HttpRequester 封裝 HttpURLConnection 的經典實作、完整回應物件 HttpResponse、並對比三方案優缺點與遷移建議。
package com.example.http;
import java.util.List;
public final class HttpResponse {
private final int statusCode;
private final String reasonPhrase;
private final String body;
private final String contentType;
private final String contentEncoding;
private final List<String> headers; // 簡化:僅儲存原始 Header 行
private final String requestUrl;
private final String method;
// 建構子、Getter 省略...
public boolean isSuccess() { return statusCode >= 200 && statusCode < 300; }
}
package com.example.http;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class HttpRequester {
private final String defaultCharset;
private final int connectTimeout;
private final int readTimeout;
public HttpRequester() {
this(StandardCharsets.UTF_8.name(), 5000, 15000);
}
public HttpRequester(String defaultCharset, int connectTimeout, int readTimeout) {
this.defaultCharset = defaultCharset;
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
}
// ---- 便利方法 ----
public HttpResponse get(String url) throws IOException {
return send(url, "GET", null, null);
}
public HttpResponse get(String url, Map<String, String> params) throws IOException {
return send(buildUrlWithParams(url, params), "GET", null, null);
}
public HttpResponse post(String url, String body) throws IOException {
return send(url, "POST", body, Map.of("Content-Type", "application/json"));
}
public HttpResponse postForm(String url, Map<String, String> form) throws IOException {
return send(url, "POST", buildFormBody(form), Map.of("Content-Type", "application/x-www-form-urlencoded"));
}
// ---- 核心送出邏輯 ----
private HttpResponse send(String url, String method, String body, Map<String, String> headers) throws IOException {
HttpURLConnection conn = null;
try {
URL urlObj = new URL(url);
conn = (HttpURLConnection) urlObj.openConnection();
conn.setRequestMethod(method);
conn.setConnectTimeout(connectTimeout);
conn.setReadTimeout(readTimeout);
conn.setUseCaches(false);
conn.setDoInput(true);
if (headers != null) {
headers.forEach(conn::setRequestProperty);
}
if ("POST".equalsIgnoreCase(method) && body != null) {
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
os.write(body.getBytes(defaultCharset));
}
}
int status = conn.getResponseCode();
String encoding = conn.getContentEncoding();
if (encoding == null) encoding = defaultCharset;
InputStream is = (status >= 400) ? conn.getErrorStream() : conn.getInputStream();
String respBody;
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, encoding))) {
respBody = br.lines().collect(Collectors.joining("\n"));
}
List<String> headerLines = new ArrayList<>();
conn.getHeaderFields().forEach((k, v) -> {
if (k != null) v.forEach(val -> headerLines.add(k + ": " + val));
});
return new HttpResponse(status, conn.getResponseMessage(), respBody,
conn.getContentType(), encoding, headerLines, url, method);
} finally {
if (conn != null) conn.disconnect();
}
}
private String buildUrlWithParams(String url, Map<String, String> params) {
if (params == null || params.isEmpty()) return url;
StringBuilder sb = new StringBuilder(url);
sb.append(url.contains("?") ? "&" : "?");
params.forEach((k, v) -> sb.append(URLEncoder.encode(k, StandardCharsets.UTF_8))
.append("=").append(URLEncoder.encode(v, StandardCharsets.UTF_8)).append("&"));
sb.setLength(sb.length() - 1);
return sb.toString();
}
private String buildFormBody(Map<String, String> form) {
return form.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "=" +
URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
}
HttpRequester client = new HttpRequester();
HttpResponse resp = client.get("https://httpbin.org/get", Map.of("q", "java"));
if (resp.isSuccess()) {
System.out.println(resp.getBody());
} else {
System.err.println("Error: " + resp.getStatusCode() + " " + resp.getReasonPhrase());
}
| 維度 | HttpURLConnection | JDK HttpClient (11+) | OkHttp 4.x |
|---|---|---|---|
| API 風格 | 同步阻塞、冗長 | 同步/非同步、Builder、函數式 | 同步/非同步、Builder、攔截器鏈 |
| HTTP/2 支援 | 無 | 內建 | 內建 (含 HTTP/3 實驗) |
| 連線池 | 簡單全域池 | 內建、可配置 | 強大、可配置、支援連線遷移 |
| 超時控制 | 連線/讀取分離 | 連線/請求/閒置全支援 | 連線/讀取/寫入/呼叫全支援 |
| 重試機制 | 無 | 無 (需 Resilience4j) | 內建可配置重試、備援路徑 |
| 攔截器/日誌 | 無 | 無 (需封裝) | 強大攔截器鏈 (簽名、日誌、壓縮) |
| Cookie 管理 | CookieManager | CookieHandler | CookieJar (更靈活) |
| 多部分上傳 | 手工拼裝 | 手工/BodyPublisher | MultipartBody.Builder 簡潔 |
| WebSocket | 無 | 內建 | 內建 |
| 依賴大小 | 0 (JDK 內建) | 0 (JDK 內建) | ~400KB (okhttp + okio) |
| 最低 JDK | 1.0 | 11 | 8+ (Android 5+) |
HttpURLConnection 封裝或引入 OkHttp (推薦),獲得現代 API 與效能。java.net.http.HttpClient,零依賴、標準化、效能優異;需攔截器/重試時整合 Resilience4j。WebClient (WebFlux) 或 RestClient (Spring 6.1+) 封裝底層 Client,提供宣告式 API、自動重試、觀測性整合。Accept-Encoding: gzip, deflate)。errorStream) 一定要讀取並記錄,否則連線無法釋放。