首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java 原生 HTTP 客戶端:GET/POST 請求完整實現與最佳實踐

     編輯:Java 綜合教學

基於 HttpURLConnection 的輕量級 HTTP 請求工具類

在不引入 Apache HttpClient、OkHttp 等第三方依賴的前提下,JDK 原生 java.net.HttpURLConnection(底層為 URLConnection)即可滿足大多數同步 HTTP 調用需求。本文提供生產就緒的工具類實現,涵蓋連接池複用、超時控制、重試機制、異常處理、響應編碼自適應等工程化細節。

一、 核心工具類設計

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;

public final class HttpClientUtil {

    private static final int DEFAULT_CONNECT_TIMEOUT = 5000;  // 5s
    private static final int DEFAULT_READ_TIMEOUT = 10000;    // 10s
    private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; Java HttpClient)";
    private static final List<String> RETRY_EXCEPTIONS = Arrays.asList(
        "java.net.SocketTimeoutException", "java.net.ConnectException", "java.io.IOException"
    );

    private HttpClientUtil() {}

    /**
     * 發送 GET 請求
     * @param url 請求地址(不含查詢參數)
     * @param params 查詢參數 Map,自動 URLEncode
     * @param headers 自定義請求頭
     * @param connectTimeout 連接超時(ms)
     * @param readTimeout 讀取超時(ms)
     * @return 響應體字符串
     * @throws HttpException 統一異常
     */
    public static String doGet(String url, Map<String, Object> params, Map<String, String> headers,
                               int connectTimeout, int readTimeout) throws HttpException {
        StringBuilder fullUrl = new StringBuilder(url);
        if (params != null && !params.isEmpty()) {
            fullUrl.append(url.contains("?") ? "&" : "?");
            boolean first = true;
            for (Entry<String, Object> e : params.entrySet()) {
                if (!first) fullUrl.append("&");
                fullUrl.append(encode(e.getKey())).append("=").append(encode(String.valueOf(e.getValue())));
                first = false;
            }
        }
        return executeRequest("GET", fullUrl.toString(), null, headers, connectTimeout, readTimeout);
    }

    /**
     * 發送 POST 請求(表單/JSON 雙模式)
     * @param url 請求地址
     * @param body 請求體:Map→application/x-www-form-urlencoded;String→JSON/自定義
     * @param contentType Content-Type,如 "application/json;charset=UTF-8"
     * @param headers 自定義請求頭
     * @return 響應體字符串
     */
    public static String doPost(String url, Object body, String contentType,
                                Map<String, String> headers,
                                int connectTimeout, int readTimeout) throws HttpException {
        byte[] requestBody = null;
        if (body instanceof Map) {
            // 表單編碼
            StringBuilder sb = new StringBuilder();
            boolean first = true;
            for (Entry<String, Object> e : ((Map<String, Object>) body).entrySet()) {
                if (!first) sb.append("&");
                sb.append(encode(e.getKey())).append("=").append(encode(String.valueOf(e.getValue())));
                first = false;
            }
            requestBody = sb.toString().getBytes(StandardCharsets.UTF_8);
            if (contentType == null) contentType = "application/x-www-form-urlencoded;charset=UTF-8";
        } else if (body instanceof String) {
            requestBody = ((String) body).getBytes(StandardCharsets.UTF_8);
        } else if (body != null) {
            throw new HttpException("Unsupported body type: " + body.getClass());
        }
        return executeRequest("POST", url, requestBody, headers, connectTimeout, readTimeout, contentType);
    }

    /** 統一執行邏輯:重試、超時、編碼檢測、資源關閉 */
    private static String executeRequest(String method, String url, byte[] body,
                                         Map<String, String> headers,
                                         int connectTimeout, int readTimeout,
                                         String... contentTypeOverride) throws HttpException {
        HttpException lastEx = null;
        for (int attempt = 0; attempt <= 2; attempt++) { // 最大重試 2 次
            HttpURLConnection conn = null;
            try {
                URL urlObj = new URL(url);
                conn = (HttpURLConnection) urlObj.openConnection();
                conn.setRequestMethod(method);
                conn.setConnectTimeout(connectTimeout > 0 ? connectTimeout : DEFAULT_CONNECT_TIMEOUT);
                conn.setReadTimeout(readTimeout > 0 ? readTimeout : DEFAULT_READ_TIMEOUT);
                conn.setRequestProperty("User-Agent", DEFAULT_USER_AGENT);
                conn.setRequestProperty("Accept", "*/*");
                conn.setRequestProperty("Connection", "Keep-Alive");

                // 自定義頭
                if (headers != null) {
                    for (Entry<String, String> h : headers.entrySet()) {
                        conn.setRequestProperty(h.getKey(), h.getValue());
                    }
                }

                // Content-Type
                if (contentTypeOverride.length > 0 && contentTypeOverride[0] != null) {
                    conn.setRequestProperty("Content-Type", contentTypeOverride[0]);
                }

                // 寫請求體
                if ("POST".equalsIgnoreCase(method) && body != null) {
                    conn.setDoOutput(true);
                    try (OutputStream os = conn.getOutputStream()) {
                        os.write(body);
                        os.flush();
                    }
                }

                // 處理響應
                int status = conn.getResponseCode();
                InputStream is = (status >= 400) ? conn.getErrorStream() : conn.getInputStream();
                String charset = detectCharset(conn.getContentType());
                String response = readStream(is, charset);

                if (status >= 400) {
                    throw new HttpException("HTTP " + status + ": " + response, status);
                }
                return response;

            } catch (Exception e) {
                lastEx = new HttpException("Request failed: " + e.getMessage(), e);
                // 可重試異常判斷
                if (attempt < 2 && RETRY_EXCEPTIONS.stream().anyMatch(e.getClass().getName()::equals)) {
                    sleepQuietly(200 * (attempt + 1)); // 指數退避
                    continue;
                }
                throw lastEx;
            } finally {
                if (conn != null) conn.disconnect();
            }
        }
        throw lastEx; // 不應到達
    }

    /** 從 Content-Type 解析 charset,默認 UTF-8 */
    private static String detectCharset(String contentType) {
        if (contentType == null) return StandardCharsets.UTF_8.name();
        String[] parts = contentType.split(";\\s*");
        for (String p : parts) {
            if (p.trim().startsWith("charset=")) {
                return p.trim().substring(8).replace("\"", "");
            }
        }
        return StandardCharsets.UTF_8.name();
    }

    private static String readStream(InputStream is, String charset) throws IOException {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(is, charset))) {
            StringBuilder sb = new StringBuilder();
            char[] buf = new char[1024];
            int len;
            while ((len = br.read(buf)) != -1) sb.append(buf, 0, len);
            return sb.toString();
        }
    }

    private static String encode(String s) {
        try { return URLEncoder.encode(s, StandardCharsets.UTF_8.name()); }
        catch (UnsupportedEncodingException e) { throw new RuntimeException(e); }
    }

    private static void sleepQuietly(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }

    /** 統一異常類 */
    public static class HttpException extends Exception {
        private final int statusCode;
        public HttpException(String msg) { this(msg, -1); }
        public HttpException(String msg, int statusCode) { super(msg); this.statusCode = statusCode; }
        public HttpException(String msg, Throwable cause) { super(msg, cause); this.statusCode = -1; }
        public int getStatusCode() { return statusCode; }
    }
}

二、 使用示例

1. 簡單 GET 請求

String result = HttpClientUtil.doGet(
    "https://api.example.com/users",
    Map.of("page", 1, "size", 20),
    null, 5000, 10000
);

2. JSON POST 請求

String json = "{\"name\":\"張三\",\"age\":25}";
String resp = HttpClientUtil.doPost(
    "https://api.example.com/users",
    json,
    "application/json;charset=UTF-8",
    Map.of("Authorization", "Bearer token123"),
    5000, 15000
);

3. 表單 POST 請求

String resp = HttpClientUtil.doPost(
    "https://api.example.com/login",
    Map.of("username", "admin", "password", "123456"),
    null, // 自動 application/x-www-form-urlencoded
    null, 3000, 5000
);

三、 關鍵工程化細節深度解析

維度處理策略說明
連接複用Keep-Alive + JVM 連接緩存設置 Connection: Keep-Alive;JDK 內置緩存由 http.keepAlive、http.maxConnections 系統屬性控制。
超時控制分離連接/讀取超時防止掛死:連接超時通常 3-5s,讀取超時視業務 10-30s。
重試機制冪等請求指數退避重試僅對 GET/幂等 POST 重試;網絡異常(SocketTimeout/ConnectException)觸發,最多 2 次。
編碼自適應Content-Type charset 解析 + UTF-8 回退避免中文亂碼;若無 charset 聲明,默認 UTF-8。
資源釋放try-with-resources + finally disconnect確保流與連接在異常時也能釋放,防止文件描述符洩漏。
異常統一自定義 HttpException 包裝狀態碼業務層捕獲單一異常,便於統一錯誤碼映射與日誌記錄。
大響應處理流式讀取避免 OOM分塊讀取(示例用 1K char buffer),超大文件建議直接寫入磁盤文件。

四、 進階擴展建議

  1. 接入連接池:高併發場景建議遷移至 Apache HttpClient 4.5+ / 5.x 或 OkHttp 4.x,支持連接池、HTTP/2、連接預熱、熔斷器整合。
  2. 異步非阻塞:JDK 11+ HttpClient (java.net.http) 支持 CompletableFuture 異步調用,WebFlux/Vert.x 場景首選。
  3. 響應式流:Reactor Netty / Spring WebClient 提供背壓支持,適合微服務網關聚合調用。
  4. 可觀測性:集成 Micrometer + Prometheus 記錄延遲、錯誤率、吞吐指標;接入 SkyWalking/Zipkin 實現分佈式追蹤。
  5. 安全加固:啟用 hostname 驗證、證書釘裝、請求/響應體脫敏日誌、防 SSRF(內網 IP 過濾)。

五、 常見坑點避雷指南

  • ❌ 忘記關閉 ErrorStream:4xx/5xx 響應需讀取 conn.getErrorStream() 並關閉,否則連接無法復用。
  • ❌ GET 請求攜帶 Body:HTTP 標準不建議 GET 帶 Body,部分代理/服務器會丟棄;參數請放 Query String。
  • ❌ 信任系統默認代理:生產環境需顯式設置 Proxy.NO_PROXY 或配置代理選項,避免請求誤走公司代理導致超時。
  • ❌ 硬編碼 User-Agent:模擬瀏覽器 UA 可能觸發反爬蟲;建議使用標識應用身份的 UA(如 MyApp/1.0 (Java 17))。
  • ❌ 忽略重定向處理:默認自動跟隨 3xx 重定向(最多 20 次),POST 重定向可能變 GET(RFC 7231),需根據業務決定是否禁用 setInstanceFollowRedirects(false)。

本工具類作為零依賴的輕量級方案,適用於中小型項目、遷移過渡期、Serverless 函數冷啟動優化等場景。大規模微服務架構建議統一接入成熟的 HTTP 客戶端庫,享受連接池、熔斷、負載均衡、可觀測性等企業級能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved