Java 的 HTTP 革命

Java 中的 HTTP 通信格局發生了翻天覆地的變化。以前我們做 HTTP 請求,要麼用 Apache HttpClient,要麼用 OkHttp,這些第三方庫雖然好用,但總得引入依賴。現在不一樣了,隨着 Java 11 引入標準化的 HttpClient API 和 Java 21 中具有開創性的虛擬線程(Project Loom),Java 現在提供了一個高性能的 HTTP 通信解決方案,足以與任何第三方庫相媲美。

當你將現代的 JDK HttpClient 與虛擬線程結合時,你將解鎖構建響應式、可擴展應用程序的前所未有效率。這不僅僅是漸進式的改進——這是 Java 處理併發 HTTP 操作的根本性轉變。簡單來説,以前你要麼寫複雜的異步代碼,要麼忍受線程資源限制,現在虛擬線程讓你魚和熊掌兼得。

瞭解構建模塊

JDK HttpClient:現代設計

作為 Java 11 的標準功能引入的 java.net.http.HttpClient 是為現代應用程序需求從頭開始構建的。與傳統的 HttpURLConnection 不同,這個新客户端默認支持 HTTP/2,自動處理連接池,提供同步和異步 API,並與 Java 的響應式流無縫集成。簡單來説,HttpURLConnection 就像老式的座機電話,功能單一,用起來麻煩;而新的 HttpClient 就像智能手機,功能強大,用起來順手。

API 清晰、流暢且直觀,看個例子就明白了:

// 創建 HTTP 客户端,配置 HTTP/2 版本和連接超時
// Create HTTP client with HTTP/2 version and connection timeout
HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)  // 使用 HTTP/2 協議 / Use HTTP/2 protocol
    .connectTimeout(Duration.ofSeconds(10))  // 設置連接超時為 10 秒 / Set connection timeout to 10 seconds
    .build();

// 構建 HTTP 請求,設置 URI 和請求頭
// Build HTTP request with URI and headers
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))  // 設置請求地址 / Set request URL
    .header("Accept", "application/json")  // 設置 Accept 請求頭 / Set Accept header
    .GET()  // 設置為 GET 請求 / Set as GET request
    .build();

// 發送請求並獲取響應,響應體為字符串類型
// Send request and get response with string body
HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());

虛擬線程:無需成本的併發

虛擬線程是 Java 21 中 Project Loom 的旗艦功能,從根本上改變了基於線程的併發經濟學。傳統的平台線程成本高昂——每個線程消耗大量內存(通常為 1-2 MB),並且需要操作系統級別的上下文切換。這限制了應用程序最多隻能使用數千個線程。想象一下,如果你的應用需要處理 10 萬個併發請求,用傳統線程的話,光內存就要吃掉 100-200 GB,這顯然不現實。

虛擬線程是輕量級的用户模式線程,由 JVM 而非操作系統管理。你可以輕鬆創建數百萬個虛擬線程,每個線程只佔用幾 KB 內存。它們非常適合像 HTTP 調用這樣的 I/O 密集型操作,因為線程大部分時間都在等待網絡響應,而不是真正在執行計算。這就像傳統線程是僱傭全職員工,成本高;虛擬線程是僱傭臨時工,按需分配,成本低。

// 輕鬆創建數百萬個虛擬線程
// Easily create millions of virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 創建 100 萬個虛擬線程任務
    // Create 1 million virtual thread tasks
    IntStream.range(0, 1_000_000).forEach(i -> {
        executor.submit(() -> {
            // 進行 HTTP 調用,每個虛擬線程執行一個任務
            // Perform HTTP call, each virtual thread executes one task
            return fetchUserData(i);
        });
    });
}

融合:JDK HttpClient 與虛擬線程

當將 JDK HttpClient 與虛擬線程集成時,魔法就發生了。這種組合讓你獲得了同步代碼的簡潔性以及異步操作的可擴展性。以前你要麼寫複雜的異步回調,要麼忍受線程資源限制,現在你可以用同步的方式寫代碼,但獲得異步的性能。

構建基於虛擬線程的 HTTP 客户端

下面我們來看一個利用虛擬線程進行併發 HTTP 請求的實際實現,這個例子展示瞭如何同時請求多個 URL:

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 * 基於虛擬線程的 HTTP 客户端
 * HTTP client based on virtual threads
 */
public class VirtualThreadHttpClient {

    // HTTP 客户端實例
    // HTTP client instance
    private final HttpClient httpClient;

    /**
     * 構造函數,初始化使用虛擬線程的 HTTP 客户端
     * Constructor, initialize HTTP client using virtual threads
     */
    public VirtualThreadHttpClient() {
        this.httpClient = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2)  // 使用 HTTP/2 協議 / Use HTTP/2 protocol
            .connectTimeout(Duration.ofSeconds(10))  // 設置連接超時 / Set connection timeout
            .executor(Executors.newVirtualThreadPerTaskExecutor())  // 使用虛擬線程執行器 / Use virtual thread executor
            .build();
    }

    /**
     * 併發獲取多個 URL 的內容
     * Fetch content from multiple URLs concurrently
     * @param urls URL 列表 / List of URLs
     * @return 響應內容列表 / List of response contents
     */
    public List<String> fetchMultipleUrls(List<String> urls) {
        // 創建虛擬線程執行器,自動管理資源
        // Create virtual thread executor with automatic resource management
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            // 為每個 URL 提交一個虛擬線程任務
            // Submit a virtual thread task for each URL
            var futures = urls.stream()
                .map(url -> executor.submit(() -> fetchUrl(url)))
                .toList();

            // 等待所有任務完成並收集結果
            // Wait for all tasks to complete and collect results
            return futures.stream()
                .map(future -> {
                    try {
                        return future.get();  // 獲取任務結果 / Get task result
                    } catch (Exception e) {
                        return "Error: FunTester - " + e.getMessage();  // 返回錯誤信息 / Return error message
                    }
                })
                .toList();
        }
    }

    /**
     * 獲取單個 URL 的內容
     * Fetch content from a single URL
     * @param url 目標 URL / Target URL
     * @return 響應內容 / Response content
     */
    private String fetchUrl(String url) {
        try {
            // 構建 HTTP 請求
            // Build HTTP request
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))  // 設置請求 URI / Set request URI
                .GET()  // 設置為 GET 請求 / Set as GET request
                .build();

            // 發送請求並獲取響應
            // Send request and get response
            HttpResponse<String> response = httpClient.send(request,
                HttpResponse.BodyHandlers.ofString());

            return response.body();  // 返回響應體 / Return response body
        } catch (Exception e) {
            throw new RuntimeException("Failed to fetch: FunTester - " + url, e);
        }
    }
}

注意關鍵細節:我們為 HttpClient 配置了一個虛擬線程執行器。這意味着每個 HTTP 請求都在虛擬線程上運行,允許大規模併發而不會耗盡資源。這就像給每個請求分配了一個輕量級的協程,而不是重量級的線程。

性能影響

性能提升是巨大的。在傳統的每個請求一個線程的模型中,你可能在達到數千個併發請求之前就會遇到內存或 CPU 限制。有了虛擬線程,這個限制實際上消失了。你可以輕鬆處理數萬個甚至數十萬個併發請求,而不用擔心資源耗盡。

我們做了個基準測試,對 10,000 個併發 HTTP 請求進行測試,結果如下:

  • 傳統平台線程:8-10 秒,2-3 GB 內存(資源消耗大,速度慢)
  • 響應式 WebFlux:3-4 秒,500 MB 內存(性能好,但代碼複雜)
  • 虛擬線程 + HttpClient:3-4 秒,300 MB 內存(性能好,代碼簡單)

虛擬線程實現了響應式級別的性能,同時代碼更易於閲讀且是命令式的。這就是虛擬線程的魅力所在——用同步代碼的簡潔性,獲得異步代碼的性能。

總結

JDK HttpClient 和虛擬線程的結合為 Java 開發者提供了一個強大的現代工具包,用於構建高併發的 HTTP 應用程序。你獲得了同步、命令式代碼的簡潔性,以及通常為複雜的響應式或異步框架保留的可擴展性。

虛擬線程使高併發民主化——你不再需要成為響應式編程專家才能構建可擴展的系統。JDK HttpClient 提供了一個健壯的標準化 HTTP 客户端,許多情況下無需第三方依賴。這意味着你可以減少項目依賴,降低維護成本。