概述
為什麼建立 Spring WebFlux?
部分原因在於需要一個非阻塞 Web 棧來使用少量執行緒處理併發並使用更少的硬體資源進行伸縮。Servlet 非阻塞 I/O 偏離了 Servlet API 的其餘部分,其中契約是同步的 (Filter
, Servlet
) 或阻塞的 (getParameter
, getPart
)。這是建立新的通用 API 的動機,該 API 可作為所有非阻塞執行時的基礎。這一點很重要,因為像 Netty 這樣的伺服器在非同步、非阻塞領域已經很成熟。
另一部分原因在於函數語言程式設計。正如 Java 5 中添加註解創造了機會(例如註解的 REST 控制器或單元測試)一樣,Java 8 中新增 Lambda 表示式也為 Java 中的函式式 API 創造了機會。這對於非阻塞應用和延續風格的 API(由 CompletableFuture
和 ReactiveX 普及)來說是一個福音,它們允許對非同步邏輯進行宣告式組合。在程式設計模型層面,Java 8 使 Spring WebFlux 能夠在註解控制器之外提供函式式 Web 端點。
定義“響應式”
我們談到了“非阻塞”和“函式式”,但是響應式意味著什麼?
術語“響應式”是指圍繞響應變化而構建的程式設計模型——網路元件響應 I/O 事件,UI 控制器響應滑鼠事件等等。從這個意義上說,非阻塞是響應式的,因為我們現在不是被阻塞,而是處於響應操作完成或資料可用通知的模式。
我們 Spring 團隊認為與“響應式”相關的另一個重要機制是非阻塞背壓。在同步的命令式程式碼中,阻塞呼叫充當了一種自然的背壓形式,強制呼叫者等待。在非阻塞程式碼中,控制事件速率變得很重要,以便快速生產者不會壓垮其目的地。
Reactive Streams 是一個小型規範(也在 Java 9 中採用),它定義了帶背壓的非同步元件之間的互動。例如,資料倉庫(充當 Publisher)可以生成資料,然後 HTTP 伺服器(充當 Subscriber)可以將資料寫入響應。Reactive Streams 的主要目的是讓 Subscriber 控制 Publisher 生成資料的快慢。
常見問題:如果 Publisher 無法減速怎麼辦? Reactive Streams 的目的僅在於建立機制和邊界。如果 Publisher 無法減速,它必須決定是緩衝、丟棄還是失敗。 |
響應式 API
Reactive Streams 在互操作性方面發揮著重要作用。它對庫和基礎設施元件有意義,但作為應用 API 用處較小,因為它層次太低。應用需要更高層、更豐富的函式式 API 來組合非同步邏輯——類似於 Java 8 Stream API,但不僅限於集合。這是響應式庫所扮演的角色。
Reactor 是 Spring WebFlux 的首選響應式庫。它提供了 Mono
和 Flux
API 型別,透過一組豐富的與 ReactiveX 運算子詞彙表對齊的運算子來處理 0..1 (Mono
) 和 0..N (Flux
) 的資料序列。Reactor 是一個 Reactive Streams 庫,因此其所有運算子都支援非阻塞背壓。Reactor 強烈關注伺服器端 Java。它與 Spring 緊密合作開發。
WebFlux 要求 Reactor 作為核心依賴,但它可以透過 Reactive Streams 與其他響應式庫互操作。作為一般規則,WebFlux API 接受普通 Publisher
作為輸入,在內部將其適配為 Reactor 型別,使用它,並返回 Flux
或 Mono
作為輸出。因此,您可以將任何 Publisher
作為輸入傳遞,並可以在輸出上應用操作,但您需要將輸出適配以與其他響應式庫一起使用。在可行的情況下(例如,註解控制器),WebFlux 透明地適配 RxJava 或其他響應式庫的使用。有關詳細資訊,請參閱響應式庫。
除了響應式 API,WebFlux 還可以與 Kotlin 中的Coroutines API 一起使用,後者提供了更命令式的程式設計風格。以下 Kotlin 程式碼示例將使用 Coroutines API 提供。 |
程式設計模型
spring-web
模組包含 Spring WebFlux 的響應式基礎,包括 HTTP 抽象、支援伺服器的 Reactive Streams 介面卡、編解碼器以及與 Servlet API 類似但具有非阻塞契約的核心 WebHandler
API。
在此基礎上,Spring WebFlux 提供了兩種程式設計模型選擇
適用性
Spring MVC 還是 WebFlux?
這是一個很自然的問題,但它構建了一個不健全的二分法。實際上,兩者協同工作以擴充套件可用選項的範圍。兩者在設計上相互連續和一致,它們可以並行使用,來自每一方的反饋都有益於雙方。下圖顯示了兩者如何關聯、它們有哪些共同點以及各自唯一支援什麼

我們建議您考慮以下具體要點
-
如果您有一個執行良好的 Spring MVC 應用,則無需更改。指令式程式設計是編寫、理解和除錯程式碼最簡單的方式。您擁有最大的庫選擇範圍,因為從歷史上看,大多數都是阻塞的。
-
如果您正在尋找非阻塞 Web 棧,Spring WebFlux 提供了與該領域其他技術相同的執行模型優勢,還提供了伺服器選擇(Netty、Tomcat、Jetty、Undertow 和 Servlet 容器)、程式設計模型選擇(註解控制器和函式式 Web 端點)以及響應式庫選擇(Reactor、RxJava 或其他)。
-
如果您對使用 Java 8 Lambda 或 Kotlin 的輕量級函式式 Web 框架感興趣,您可以使用 Spring WebFlux 函式式 Web 端點。這對於需求不太複雜的小型應用或微服務來說也是一個不錯的選擇,可以從更高的透明度和控制中受益。
-
在微服務架構中,您可以混合使用包含 Spring MVC 或 Spring WebFlux 控制器或 Spring WebFlux 函式式端點的應用。在兩個框架中支援相同的基於註解的程式設計模型,可以在選擇合適的工具完成合適的工作的同時,更容易複用知識。
-
評估應用的一個簡單方法是檢查其依賴項。如果您有阻塞的持久化 API(JPA、JDBC)或網路 API 要使用,至少對於常見架構而言,Spring MVC 是最佳選擇。使用 Reactor 和 RxJava 在單獨的執行緒上執行阻塞呼叫在技術上是可行的,但您將無法充分利用非阻塞 Web 棧。
-
如果您有一個呼叫遠端服務的 Spring MVC 應用,嘗試使用響應式
WebClient
。您可以直接從 Spring MVC 控制器方法返回響應式型別(Reactor、RxJava 或其他)。每次呼叫的延遲或呼叫之間的相互依賴性越大,效益就越顯著。Spring MVC 控制器也可以呼叫其他響應式元件。 -
如果您的團隊規模較大,請記住轉向非阻塞、函式式和宣告式程式設計的學習曲線很陡峭。在不完全切換的情況下開始的一個實用方法是使用響應式
WebClient
。除此之外,從小處著手並衡量收益。我們預計,對於許多應用而言,這種轉變是不必要的。如果您不確定要尋找哪些好處,可以先了解非阻塞 I/O 的工作原理(例如,單執行緒 Node.js 上的併發)及其影響。
伺服器
Spring WebFlux 支援 Tomcat、Jetty、Servlet 容器以及 Netty 和 Undertow 等非 Servlet 執行時。所有伺服器都適配到一個低階的通用 API,以便可以在不同伺服器上支援更高級別的程式設計模型。
Spring Boot 有一個 WebFlux starter,可以自動化這些步驟。預設情況下,該 starter 使用 Netty,但透過更改 Maven 或 Gradle 依賴項,可以輕鬆切換到 Tomcat、Jetty 或 Undertow。Spring Boot 預設使用 Netty,因為它在非同步非阻塞領域更廣泛使用,並且允許客戶端和伺服器共享資源。
Tomcat 和 Jetty 可以與 Spring MVC 和 WebFlux 一起使用。但請記住,它們的使用方式非常不同。Spring MVC 依賴於 Servlet 阻塞 I/O,如果需要,允許應用直接使用 Servlet API。Spring WebFlux 依賴於 Servlet 非阻塞 I/O,並在低階介面卡後面使用 Servlet API。它不暴露用於直接使用。
強烈建議不要在 WebFlux 應用上下文中對映 Servlet 過濾器或直接操作 Servlet API。由於上述原因,在同一上下文中混合阻塞 I/O 和非阻塞 I/O 會導致執行時問題。 |
對於 Undertow,Spring WebFlux 直接使用 Undertow API,不使用 Servlet API。
效能
效能有許多特徵和含義。響應式和非阻塞通常不會讓應用執行得更快。在某些情況下可以實現——例如,如果使用 WebClient
並行執行遠端呼叫。然而,以非阻塞方式做事需要更多工作,這可能會稍微增加所需的處理時間。
響應式和非阻塞的關鍵預期好處是能夠使用少量固定數量的執行緒和更少的記憶體進行伸縮。這使得應用在負載下更具彈性,因為它們以更可預測的方式伸縮。然而,為了觀察這些好處,您需要存在一些延遲(包括慢速和不可預測的網路 I/O 的混合)。在這裡,響應式棧開始顯示其優勢,並且差異可能非常顯著。
併發模型
Spring MVC 和 Spring WebFlux 都支援註解控制器,但在併發模型以及對阻塞和執行緒的預設假設方面存在關鍵差異。
在 Spring MVC(以及一般的 Servlet 應用)中,假設應用可以阻塞當前執行緒,例如,進行遠端呼叫。因此,Servlet 容器使用大型執行緒池來吸收請求處理期間的潛在阻塞。
在 Spring WebFlux(以及一般的非阻塞伺服器)中,假設應用不會阻塞。因此,非阻塞伺服器使用少量固定大小的執行緒池(事件迴圈工作執行緒)來處理請求。
“伸縮”和“少量執行緒”可能聽起來矛盾,但永遠不阻塞當前執行緒(而是依賴回撥)意味著您不需要額外的執行緒,因為沒有要吸收的阻塞呼叫。 |
呼叫阻塞 API
如果確實需要使用阻塞庫怎麼辦?Reactor 和 RxJava 都提供了 publishOn
運算子,用於在不同的執行緒上繼續處理。這意味著有一個簡單的逃生通道。但請記住,阻塞 API 不適合此併發模型。
可變狀態
在 Reactor 和 RxJava 中,您透過運算子宣告邏輯。在執行時,會形成一個響應式管道,資料在不同階段順序處理。這樣做的一個關鍵好處是,它使應用無需保護可變狀態,因為該管道中的應用程式碼永遠不會併發呼叫。
執行緒模型
在執行 Spring WebFlux 的伺服器上,您應該期望看到哪些執行緒?
-
在“純淨的” Spring WebFlux 伺服器上(例如,沒有資料訪問或其他可選依賴項),您可以期望一個伺服器執行緒和幾個用於請求處理的執行緒(通常與 CPU 核心數相同)。然而,Servlet 容器可能會啟動更多執行緒(例如,Tomcat 上有 10 個),以支援 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 使用。
-
響應式
WebClient
以事件迴圈風格執行。因此,您可以看到與之相關的少量固定處理執行緒(例如,使用 Reactor Netty 聯結器的reactor-http-nio-
)。然而,如果 Reactor Netty 用於客戶端和伺服器,兩者預設共享事件迴圈資源。 -
Reactor 和 RxJava 提供了執行緒池抽象,稱為排程器(schedulers),與
publishOn
運算子一起使用,後者用於將處理切換到不同的執行緒池。這些排程器的名稱暗示了特定的併發策略——例如,“parallel”(用於 CPU 密集型工作,執行緒數有限)或“elastic”(用於 I/O 密集型工作,執行緒數較多)。如果你看到這樣的執行緒,意味著某些程式碼正在使用特定的執行緒池Scheduler
策略。 -
資料訪問庫和其他第三方依賴項也可以建立和使用自己的執行緒。