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 客户端,許多情況下無需第三方依賴。這意味着你可以減少項目依賴,降低維護成本。