
有個群里聊到一個挺有意思的問題既然 Apache HttpAsyncClient 也是異步 HTTP 客戶端能不能直接替換掉 Spring WebFlux 的 WebClient畢竟很多團(tuán)隊嘴上說著響應(yīng)式實際寫代碼還是 Future、回調(diào)那一套WebClient 的 Mono/Flux 鏈?zhǔn)綄懛ù_實勸退了不少人。這個問題背后其實是在問兩件事兩個客戶端到底差了些什么以及我的項目到底屬于哪種異步場景。今天就把這兩個東西掰開揉碎對比一遍順便結(jié)合我實際遷移和排障的經(jīng)驗聊聊什么時候該用誰什么時候千萬別硬換。先說一個結(jié)論HttpAsyncClient 和 WebClient 都是異步、非阻塞的 HTTP 客戶端但它們的異步模型完全不是一個流派。HttpAsyncClient 走的是“回調(diào) Future”的傳統(tǒng)異步路線WebClient 走的是“響應(yīng)式流 背壓”的 Reactive Streams 路線。單看“能不能替代”答案是有條件地能但你要是想在一個 WebFlux 全鏈路的項目里無腦替換那大概率會把簡單事情搞復(fù)雜甚至讓整個調(diào)用鏈的響應(yīng)式語義徹底崩掉。下面我會分五塊來聊每一塊都會給出能直接落地的代碼和配置也會把踩過的坑交代清楚。1. 先搞清楚兩個客戶端的本質(zhì)差異1.1 HttpAsyncClient 是什么、怎么運作HttpAsyncClient 是 Apache HttpComponents 旗下的異步 HTTP 客戶端算得上后端江湖里的老牌異步組件了。它底層基于 NIO發(fā)起請求以后不會卡住當(dāng)前線程而是通過回調(diào)或者 Future 在請求完成后再拿到結(jié)果。最典型的用法是這樣CloseableHttpAsyncClient client HttpAsyncClients.custom() .setMaxConnPerRoute(20) .setMaxConnTotal(200) .build(); client.start(); HttpGet request new HttpGet(https://example.com/api/data); FutureHttpResponse future client.execute(request, null); // 注意future.get() 是阻塞的在這里調(diào)了就是同步 HttpResponse response future.get();不想阻塞的話可以傳入一個 FutureCallback請求完成時框架會在自己的 I/O 線程里回調(diào)你client.execute(request, new FutureCallbackHttpResponse() { Override public void completed(HttpResponse result) { // 這里可以處理響應(yīng) } Override public void failed(Exception ex) { // 異常處理 } Override public void cancelled() { // 取消處理 } });這種模式眼熟吧很多老框架用了一輩子這種套路。它的優(yōu)點是很直觀一下就能看懂請求發(fā)出去、結(jié)果回來之后干什么。缺點也同樣直觀一旦業(yè)務(wù)復(fù)雜起來回調(diào)嵌套、多個請求并發(fā)歸并、重試、超時這些事都得你自己拿代碼去湊寫起來非常容易失控。1.2 WebClient 是什么、怎么運作WebClient 是 Spring WebFlux 給出來的響應(yīng)式 HTTP 客戶端。它跑在 Reactor 之上返回的全部是 Mono 或 Flux也就是“發(fā)布者”。你調(diào)一次 WebClient 并不會立即發(fā)起請求只是定義了一個數(shù)據(jù)流等下游真正訂閱的時候才會觸發(fā)網(wǎng)絡(luò)調(diào)用。典型寫法長這樣WebClient webClient WebClient.builder() .baseUrl(https://example.com) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); MonoString result webClient.get() .uri(/api/data) .retrieve() .bodyToMono(String.class);光看這段你可能會覺得和普通聲明式 API 差不多。但 WebClient 真正的殺傷力在于它能把多個請求組合成一個響應(yīng)式鏈路然后統(tǒng)一編排超時、重試、錯誤恢復(fù)和并發(fā)執(zhí)行Mono.zip( webClient.get().uri(/api/a).retrieve().bodyToMono(A.class), webClient.get().uri(/api/b).retrieve().bodyToMono(B.class) ) .map(tuple - combine(tuple.getT1(), tuple.getT2())) .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))) .timeout(Duration.ofSeconds(5)) .onErrorResume(e - Mono.just(fallbackValue));這一串操作在 WebClient 里就是幾個 operator放在 HttpAsyncClient 里你要寫多少回調(diào)、多少共享狀態(tài)、多少個 CountDownLatch 才能把并發(fā)歸并做完想都不敢想。1.3 真正的分水嶺回調(diào)模型與響應(yīng)式模型所以你會發(fā)現(xiàn)這兩個客戶端表面上看都是“發(fā)起異步請求、不阻塞線程”但本質(zhì)區(qū)別在于程序設(shè)計范式。HttpAsyncClient 的異步終點是 Future 和回調(diào)它解決的是“別讓線程傻等 I/O”的問題。你拿到 Future 以后仍需自己考慮如何把多個異步結(jié)果組合起來或者如何優(yōu)雅處理失敗。雖然也能搭配 CompletableFuture 做編排但這是從外部把它硬包成響應(yīng)式或組合式的樣子核心還是命令式的。WebClient 的異步終點是 Publisher它強調(diào)的是“數(shù)據(jù)流動鏈路”。發(fā)請求只是整個響應(yīng)式流中的一個節(jié)點上游、下游天然帶有背壓機制你可以用 flatMap、zip、retryWhen、timeout 這些操作符像搭積木一樣搭出一個聲明式的執(zhí)行流程。麻煩也是顯而易見的學(xué)習(xí) Reactor 的思維模型比學(xué)會 HttpClient API 要難得多。不少人把“異步”當(dāng)成一個統(tǒng)一概念認(rèn)為換了客戶端就換了異步姿勢。實際上從 HttpAsyncClient 切到 WebClient或者反過來切你改變的不僅是 API而是整條調(diào)用鏈的編程模型。這個認(rèn)知如果不建立起來后面任何一個遷移方案都會踩坑。2. 逐項對比從 API 風(fēng)格到生態(tài)集成2.1 編程模型與 API 風(fēng)格的差距先看最簡單的“單個請求同步等結(jié)果”。HttpAsyncClient 里最典型的寫法就是future.get()這一行代碼下去當(dāng)前線程就阻塞了所以嚴(yán)格說這只是“支持異步但允許你同步等待”。而 WebClient 如果非要同步拿結(jié)果得先block()再返回比如webClient.get()...block()這種寫法在 WebFlux 服務(wù)端里屬于高危動作但放在普通 Spring MVC 里倒是能用。再看復(fù)雜編排。我用 HttpAsyncClient 做過一次多接口聚合兩個請求并發(fā)出去然后等兩個都回來以后做合并當(dāng)時的偽代碼大概是這樣FutureHttpResponse futureA client.execute(requestA, null); FutureHttpResponse futureB client.execute(requestB, null); HttpResponse responseA futureA.get(); HttpResponse responseB futureB.get(); // 合并處理這看起來沒什么但兩個get()是串行阻塞的。要是想真正并發(fā)等待還得用CompletableFuture.allOf(...)把兩個 Future 包一層。一旦加上超時、重試、熔斷代碼復(fù)雜度會指數(shù)級上升。而 WebClient 用Mono.zip和retryWhen幾行就搞定代碼可讀性完全不在一個量級。不過這里要公平說一句并不是所有項目都需要復(fù)雜編排。如果你的場景就是“發(fā)一個請求拿一個響應(yīng)完事”HttpAsyncClient 的 API 比 WebClient 的 Flux/Mono 鏈要直白得多。尤其團(tuán)隊里都是寫慣命令式代碼的人讓他們寫 Reactor 反而會弄出一堆反模式比如在subscribe里拋異常、把block()塞到循環(huán)里等等。2.2 與 Spring 生態(tài)的集成深度這是 WebClient 的主場。在 Spring Boot 項目里WebClient 可以通過Configuration和Bean方便地配置連接池、超時、過濾器、攔截器也能直接拿來和 WebTestClient 配合做接口測試。更重要的是當(dāng)你的服務(wù)端是 WebFlux 的 Controller 時Controller 可以直接 return 一個 WebClient 返回的 Mono整個請求鏈路從入口到出口都不阻塞也不需要手動適配類型。HttpAsyncClient 在這塊就明顯吃虧了。它和 Spring 生態(tài)沒有原生綁定你要自己管理客戶端的創(chuàng)建、啟動、關(guān)閉自己處理依賴注入。在 Spring MVC 項目里把它當(dāng)普通 Bean 用倒也行但服務(wù)的關(guān)閉鉤子、連接池的銷毀、異常響應(yīng)體的解析都要自己寫。比如最基礎(chǔ)的一個問題HttpAsyncClient 拿到響應(yīng)后響應(yīng)體是HttpEntity需要你自己轉(zhuǎn)成字符串、解析 JSONWebClient 加上 Jackson 之后bodyToMono(SomeDto.class)直接幫你反序列化。這個差距在寫業(yè)務(wù)代碼的時候會非常明顯。如果你處于一個全鏈路 WebFlux 項目里上游服務(wù)返回的就是 Flux/Mono服務(wù)端也要求你返回響應(yīng)式類型那 WebClient 就是為這個鏈路設(shè)計的零件。硬塞一個 HttpAsyncClient 進(jìn)去你得像做“轉(zhuǎn)接插頭”一樣把 Future 轉(zhuǎn)成 Mono把回調(diào)包裝成響應(yīng)式流。能做嗎能做但每次看到那堆橋接代碼你都會懷疑自己是不是在自討苦吃。2.3 性能與連接管理的對比性能方面兩者在絕大多數(shù)普通業(yè)務(wù)場景下差異并不大因為他們底層都是非阻塞 I/O。真正拉開體驗差距的是連接管理、超時控制、協(xié)議支持和背壓。HttpAsyncClient 通過PoolingAsyncClientConnectionManager管理連接池。你可以精確控制最大連接數(shù)、單路由最大連接數(shù)、連接 TTL這套連接池從 Apache HttpClient 時代就一路打磨到今天成熟度相當(dāng)高。PoolingAsyncClientConnectionManager connectionManager PoolingAsyncClientConnectionManagerBuilder.create() .setMaxConnTotal(200) .setMaxConnPerRoute(20) .build(); CloseableHttpAsyncClient client HttpAsyncClients.custom() .setConnectionManager(connectionManager) .build();WebClient 默認(rèn)走 Reactor Netty也有自己的連接池配置方式但 API 完全是另一套。它的底層 HttpClient 需要單獨配置然后再包成 ReactorClientHttpConnector最后給到 WebClientConnectionProvider connectionProvider ConnectionProvider.builder(myPool) .maxConnections(200) .pendingAcquireMaxCount(100) .build(); HttpClient httpClient HttpClient.create(connectionProvider) .responseTimeout(Duration.ofSeconds(5)); WebClient webClient WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .build();HTTP/2 支持上WebClient 的 Reactor Netty 鏈路算是自然的配置一下就啟用HttpAsyncClient 要單獨引入額外的 HTTP/2 模塊配置更繁瑣一些而且不少老版本默認(rèn)還是以 HTTP/1.1 為主。如果你的線上服務(wù)大量使用 HTTP/2 多路復(fù)用WebClient 在這塊會省心一些。背壓這塊就更不用多說了。WebClient 作為響應(yīng)式客戶端天然支持按需消費響應(yīng)流你訂閱多少就消費多少不會把整個響應(yīng)體一次性塞進(jìn)內(nèi)存。HttpAsyncClient 的 Future 模型沒有背壓概念響應(yīng)體要么整體拿到要么自己控制處理速度。遇到超大響應(yīng)體下載的場景WebClient 的按需消費優(yōu)勢會立刻體現(xiàn)出來。2.4 可測試性與錯誤處理日常開發(fā)里可測試性很重要。Spring 項目測試 WebClient 有現(xiàn)成的 WebTestClient可以直接綁定到本地服務(wù)或者 Mock 的響應(yīng)體上跟 MockMvc 風(fēng)格一脈相承。WebClient 鏈?zhǔn)?API 也能很容易地注入到 Service 里做單元測試。HttpAsyncClient 測試也不是不行像 WireMock 這類工具照樣可以用。但麻煩在于異步回調(diào)函數(shù)的線程模型測試?yán)锝?jīng)常要配合 CountDownLatch 或者輪詢?nèi)サ然卣{(diào)結(jié)果寫起來比同步測試費勁。而且一旦代碼里大量使用 FutureCallback你還要額外考慮回調(diào)里的異常如何拋回測試線程搞不好就出現(xiàn)測試“假綠”其實回調(diào)壓根沒執(zhí)行完。錯誤處理方面WebClient 在鏈路里統(tǒng)一用onErrorResume、onErrorReturn、onErrorMap做恢復(fù)或者轉(zhuǎn)換。HttpAsyncClient 則是你在回調(diào)里自己判斷狀態(tài)碼、自己捕捉異常。代碼組織得差一些的話錯誤處理邏輯會散落在不同的匿名內(nèi)部類里后期維護(hù)成本比較高。我把兩者核心差異匯成一張對照表方便各取所需維度HttpAsyncClientWebClient編程模型Callback FutureReactive Streams / Mono / Flux復(fù)雜編排能力弱需要手寫組合強操作符豐富Spring 生態(tài)集成弱需要手動管理強與 WebFlux 無縫銜接內(nèi)置連接池PoolingAsyncClientConnectionManagerReactor Netty ConnectionProviderHTTP/2 支持需額外配置集成相對方便響應(yīng)式背壓不支持原生支持測試生態(tài)通用 Mock 工具WebTestClient 等學(xué)習(xí)曲線簡單直接相對陡峭3. 到底能不能替代關(guān)鍵看場景3.1 這些場景下可以替代如果項目本身不是 WebFlux 全鏈路而是普通的 Spring MVC 工程也不追求用 Reactor 做響應(yīng)式編程只是想找一個“異步發(fā) HTTP 請求”的工具那 HttpAsyncClient 完全可以用。比如定時任務(wù)里批量拉取數(shù)據(jù)、消息監(jiān)聽器里調(diào)外部服務(wù)、Netty 或者自研的異步網(wǎng)關(guān)里做 HTTP 轉(zhuǎn)發(fā)這些場景都不需要響應(yīng)式鏈?zhǔn)骄幣臜ttpAsyncClient 反而更簡潔。再比如團(tuán)隊里沒有人熟悉 Reactor硬上 WebClient 很容易寫出各種反模式。這種情況下HttpAsyncClient 用 Future 回調(diào)的方式至少能讓所有人看得懂、會維護(hù)。只要在代碼里約定好“拿到 Future 之后不能立刻 get 阻塞”異步的收益一樣能拿到。沒有背壓不代表必然內(nèi)存爆炸對于普通接口調(diào)用響應(yīng)體本來就不大背壓的意義沒有你想象中那么關(guān)鍵。3.2 這些場景下千萬別硬換最典型的反面場景就是 WebFlux 服務(wù)端。你的 Controller 已經(jīng)返回 Mono/Flux業(yè)務(wù) Service 用的也是 Reactor整個調(diào)用鏈都是非阻塞的。這時候你把 WebClient 換成 HttpAsyncClient要處理的第一件事就是把 Future 橋接成 Mono。最蠢的寫法是直接future.get()再包裝成 Mono那等同于把異步調(diào)用重新變成同步阻塞整個 WebFlux 的非阻塞優(yōu)勢毀于一旦。正確的橋接方式是用Mono.fromFuture()把 Future 轉(zhuǎn)換成 Mono。但注意HttpAsyncClient 的 execute 返回的是FutureHttpResponse并不是 Java 里的 CompletableFuture。你還需要自己把它包裝成 CompletableFuture才能在Mono.fromFuture()里用。這個包裝代碼要寫多久不算長但它把你從響應(yīng)式流拉回命令式接著還要再手動處理響應(yīng)體的流式讀取。相當(dāng)于每個請求你都要在兩種模型之間來回橫跳代碼會變得非常割裂。還有一種場景是大量調(diào)用需要按流量自動降級、熔斷、聚合指標(biāo)。WebClient 和 Reactor 的鏈路天然能配合很多響應(yīng)式組件做這些事HttpAsyncClient 沒有標(biāo)準(zhǔn)機制得自己寫攔截器、埋點、計數(shù)工作量完全是兩碼事。3.3 混用方案橋接兩個世界的套路如果你的項目確實已經(jīng)用了 WebFlux 服務(wù)端又因為某些歷史原因必須調(diào)用 HttpAsyncClient 維護(hù)的老接口不是不能混用但建議嚴(yán)格控制橋接層。我的做法是單獨封裝一個“異步適配器”把 HttpAsyncClient 的響應(yīng)封裝成 Mono不把 HttpClient 本身泄露到業(yè)務(wù)代碼里。適配器的核心思路很簡單用CompletableFuture承接異步結(jié)果再交給Mono.fromFuture()轉(zhuǎn)成響應(yīng)式發(fā)布者。大致骨架如下CompletableFutureString future new CompletableFuture(); client.execute(request, new FutureCallbackHttpResponse() { Override public void completed(HttpResponse result) { future.complete(EntityUtils.toString(result.getEntity())); } Override public void failed(Exception ex) { future.completeExceptionally(ex); } Override public void cancelled() { future.cancel(true); } }); MonoString mono Mono.fromFuture(future);這樣 WebFlux 的業(yè)務(wù)層不用關(guān)心回調(diào)細(xì)節(jié)所有調(diào)用看起來和其他異步響應(yīng)式方法一致。但這只是應(yīng)急方案橋接層越多響應(yīng)式編程的優(yōu)勢損耗就越大。如果從零開始的新項目我更推薦從一開始就用 WebClient。4. 實操踩坑與排查實錄4.1 HttpAsyncClient 使用中我踩過的坑第一個坑就是“回調(diào)里做重活”。FutureCallback 是跑在 HttpAsyncClient 內(nèi)部 I/O 線程上的你在 completed 里寫日志、寫數(shù)據(jù)庫、做耗時計算會直接拖垮客戶端的響應(yīng)處理能力。遇到一次內(nèi)部接口偶發(fā)超時排查半天結(jié)果發(fā)現(xiàn)是我自己在回調(diào)里同步調(diào)用了一個外部服務(wù)導(dǎo)致 I/O 線程全被占滿。正確做法是回調(diào)里只做輕量操作重邏輯交給自己的業(yè)務(wù)線程池。第二個坑是不手動關(guān)閉客戶端。CloseableHttpAsyncClient實現(xiàn)了 Closeable使用完要關(guān)閉。很多老代碼在每次請求時 new 一個客戶端用完也不關(guān)連接池直接泄漏。后來我統(tǒng)一把客戶端做成單例 Bean在 Spring 容器的銷毀方法里執(zhí)行 close連接問題才徹底消失。第三個坑是超時配置。HttpAsyncClient 的連接超時、Socket 超時都有獨立參數(shù)但很多人只配了連接超時沒配請求超時結(jié)果某個下游服務(wù) hang 住整個線程池掛滿。記住異步客戶端尤其要配置絕對超時否則懸著的請求會慢慢吃掉所有連接。4.2 WebClient 使用中我踩過的坑WebClient 的坑也不少。最經(jīng)典的是“看起來沒配超時實際請求永遠(yuǎn)不超時”。WebClient 本身沒有全局超時概念必須在底層 HttpClient 上設(shè)置 responseTimeout否則遠(yuǎn)端一直不響應(yīng)訂閱方可能無限等下去。我見過線上告警查到這條線一個個改配置才恢復(fù)。第二個坑是使用block()的時機。在 WebFlux 服務(wù)端或 Reactor 鏈路里調(diào)用 block會直接拋異?;蚩ㄋ馈S行┩聫?Spring MVC 轉(zhuǎn)過來不習(xí)慣習(xí)慣性地想拿同步結(jié)果結(jié)果代碼在 Test 環(huán)境沒問題、生產(chǎn)一壓測就出詭異現(xiàn)象。后來我們在 Code Review 里直接規(guī)定WebFlux 鏈路代碼禁止出現(xiàn) block除非在測試線程中使用。第三個坑是連接池耗盡的表現(xiàn)。WebClient 底層連接池如果不夠并發(fā)上來后請求會排隊等待看起來像超時但實際上是 pendingAcquire 配置太低。排查連接池參數(shù)和實際并發(fā)量的關(guān)系比調(diào)代碼重要得多。我把這些常見問題整理成一個小速查表方便你直接對照自查現(xiàn)象可能原因排查思路請求一直不返回連接池排隊堆積 / 超時未配置檢查連接池最大連接數(shù)和 responseTimeoutHttpAsyncClient 吞吐量低回調(diào)里做了耗時操作把重邏輯移到業(yè)務(wù)線程池回調(diào)保持輕量WebClient 全鏈路偶發(fā)卡死某個請求沒有響應(yīng)式超時給底層 HttpClient 配置 responseTimeout請求出現(xiàn) Connection pool shut downHttpAsyncClient 被提前 close客戶端做成單例應(yīng)用停止時再關(guān)閉WebFiux Controller 里 block 拋異常在非阻塞通路調(diào)用阻塞方法刪掉 block改用 Mono 返回值或 subscribe響應(yīng)報文很大但內(nèi)存占用高讀取整個響應(yīng)體到內(nèi)存用 Flux 流式處理或限制 buffer 大小4.3 從 WebClient 切到 HttpAsyncClient 的典型報錯如果你真決定從 WebClient 遷移到 HttpAsyncClient大部分代碼會死在類型不兼容上。WebClient 方法返回 MonoController 可以直接返回這個 MonoOrder 由 WebFlux 自動訂閱。換成 HttpAsyncClient 后你要么把 Controller 改成普通對象等于把響應(yīng)式端點改成阻塞端點要么自己做橋接。不少團(tuán)隊遷移到一半發(fā)現(xiàn) Controller 和 Service 里全是Mono.fromFuture和future.get()的混合體一壓測性能比原來躺著寫同步代碼還差。另一個常見報錯是IllegalStateException: block()/blockFirst()/blockLast() are blocking。這個常見于原先 WebClient 代碼配合.block()同步取結(jié)果切到 HttpAsyncClient 后又用 Future 的 get最終在 WebFlux 請求線程里卡死。本質(zhì)原因不是客戶端選型而是沒有搞清當(dāng)前線程是什么線程模型。所以遷移之前務(wù)必先把調(diào)用鏈拆開明確每一層是阻塞線程還是非阻塞線程。最怕的是 WebFlux 鏈路里混進(jìn)future.get()這種代碼既不響應(yīng)式也不異步純粹是拿異步 API 做同步操作性能和可維護(hù)性全丟。5. 個人結(jié)論與實踐建議回到標(biāo)題那個直白的問題HttpAsyncClient 能不能替代 WebClient我的答案是如果只在“發(fā)異步 HTTP 請求”這個最小職能上能但如果放到 WebFlux 生態(tài)、響應(yīng)式鏈路、復(fù)雜編排和背壓場景里替代的代價遠(yuǎn)高于收益。我更愿意把它看成兩個不同時代的產(chǎn)物而不是同一服務(wù)的一對競品。我的實際建議是新項目如果已經(jīng)選擇 Spring Boot 且服務(wù)端可能要上 WebFlux那就直接用 WebClient別繞路。團(tuán)隊不會 Reactor 不是換客戶端的理由找兩三天時間集中培訓(xùn)一下比后期在兩種異步模型之間反復(fù)橫跳要劃算得多。老項目如果只是想把同步 HTTP 調(diào)用換成異步不想引入響應(yīng)式編程那 HttpAsyncClient 是非??煽康倪x擇。它成熟、穩(wěn)定、連接池功能扎實在非響應(yīng)式工程里完全可以支撐起高質(zhì)量異步調(diào)用。最后再分享一個我實踐過的折中方案在少數(shù)必須混用的項目里先封裝一個統(tǒng)一的 HttpClient 接口底層分別提供 HttpAsyncClient 實現(xiàn)和 WebClient 實現(xiàn)業(yè)務(wù)代碼只依賴接口。這樣你可以在非響應(yīng)式模塊里用 HttpAsyncClient在 WebFlux 模塊里用 WebClient切換時不用到處改業(yè)務(wù)代碼。接口的設(shè)計也不用復(fù)雜暴露請求參數(shù)、返回 CompletableFuture 或者 Mono 的適配方法就夠了。等哪一天團(tuán)隊真正統(tǒng)一到響應(yīng)式模型你可以把另一套實現(xiàn)收起來業(yè)務(wù)層幾乎無感。這個方案雖然不是最優(yōu)雅但在真實的存量項目里遠(yuǎn)比“一刀切換掉”靠譜。