概觀

為何建立 Spring WebFlux?

部分答案是非阻塞式 Web Stack 的需求,以便用少量的執行緒處理並行,並以更少的硬體資源進行擴展。Servlet 非阻塞式 I/O 將我們從 Servlet API 的其餘部分引開,在 Servlet API 中,合約是同步的 (Filter, Servlet) 或阻塞式的 (getParameter, getPart)。這是建立新的通用 API 的動機,以作為任何非阻塞式執行期的基礎。這很重要,因為有些伺服器 (例如 Netty) 在非同步、非阻塞式領域中已確立地位。

另一個答案是函數式程式設計。就像 Java 5 中新增註解創造了機會 (例如註解式 REST 控制器或單元測試) 一樣,Java 8 中新增 Lambda 運算式為 Java 中的函數式 API 創造了機會。這對於非阻塞式應用程式和延續式 API (如 CompletableFutureReactiveX 所推廣的) 來說是一大福音,它們允許宣告式組合非同步邏輯。在程式設計模型層面,Java 8 使 Spring WebFlux 能夠提供函數式 Web 端點以及註解式控制器。

定義「反應式」

我們談到了「非阻塞式」和「函數式」,但反應式是什麼意思?

術語「反應式」指的是圍繞對變更做出反應而建構的程式設計模型 — 網路元件對 I/O 事件做出反應,UI 控制器對滑鼠事件做出反應等等。從這個意義上說,非阻塞式是反應式的,因為我們現在處於對操作完成或資料可用時的通知做出反應的模式,而不是被阻塞。

Spring 團隊認為與「反應式」相關的另一個重要機制是非阻塞式背壓。在同步、命令式程式碼中,阻塞呼叫充當一種自然形式的背壓,迫使呼叫者等待。在非阻塞式程式碼中,控制事件速率變得非常重要,以避免快速生產者壓垮其目的地。

Reactive Streams 是一個 小型規範 (也在 Java 9 中 採用),它定義了具有背壓的非同步元件之間的互動。例如,資料儲存庫 (充當 Publisher) 可以產生資料,而 HTTP 伺服器 (充當 Subscriber) 則可以將資料寫入回應。Reactive Streams 的主要目的是讓訂閱者控制發布者產生資料的速度。

常見問題:如果發布者無法減速怎麼辦?
Reactive Streams 的目的僅在於建立機制和邊界。如果發布者無法減速,則必須決定是否要緩衝、捨棄或失敗。

反應式 API

Reactive Streams 在互通性方面發揮著重要作用。它對於程式庫和基礎架構元件很有用,但作為應用程式 API 用處較小,因為它層次太低。應用程式需要更高層次、更豐富、更函數式的 API 來組合非同步邏輯 — 類似於 Java 8 Stream API,但不僅適用於集合。這就是反應式程式庫所扮演的角色。

Reactor 是 Spring WebFlux 的首選反應式程式庫。它提供了 MonoFlux API 類型,透過一組與 ReactiveX 運算子詞彙表 對齊的豐富運算子,處理 0..1 (Mono) 和 0..N (Flux) 的資料序列。Reactor 是一個 Reactive Streams 程式庫,因此,它的所有運算子都支援非阻塞式背壓。Reactor 非常注重伺服器端 Java。它是與 Spring 密切合作開發的。

WebFlux 需要 Reactor 作為核心依賴項,但它透過 Reactive Streams 與其他反應式程式庫具有互通性。一般來說,WebFlux API 接受純 Publisher 作為輸入,在內部將其調整為 Reactor 類型,使用該類型,並傳回 FluxMono 作為輸出。因此,您可以傳遞任何 Publisher 作為輸入,並且可以在輸出上應用操作,但您需要調整輸出以與另一個反應式程式庫一起使用。在可行的情況下 (例如,註解式控制器),WebFlux 會透明地適應 RxJava 或另一個反應式程式庫的使用。有關更多詳細資訊,請參閱 反應式程式庫

除了反應式 API 之外,WebFlux 還可以與 Kotlin 中的 協程 API 一起使用,協程 API 提供更命令式的程式設計風格。以下 Kotlin 程式碼範例將與協程 API 一起提供。

程式設計模型

spring-web 模組包含 Spring WebFlux 的基礎反應式基礎,包括 HTTP 抽象、對支援伺服器的 Reactive Streams 適配器編解碼器,以及與 Servlet API 相當但具有非阻塞式合約的核心 WebHandler API

在此基礎上,Spring WebFlux 提供了兩種程式設計模型的選擇

  • 註解式控制器:與 Spring MVC 一致,並基於 spring-web 模組中的相同註解。Spring MVC 和 WebFlux 控制器都支援反應式 (Reactor 和 RxJava) 傳回類型,因此,很難將它們區分開來。一個值得注意的區別是 WebFlux 也支援反應式 @RequestBody 引數。

  • [webflux-fn]:基於 Lambda、輕量級且函數式的程式設計模型。您可以將其視為應用程式可以用於路由和處理請求的小型程式庫或一組工具。與註解式控制器的最大區別在於,應用程式負責從頭到尾處理請求,而不是透過註解宣告意圖並被回呼。

適用性

Spring MVC 還是 WebFlux?

一個很自然會問的問題,但這建立了一個不健全的二分法。實際上,兩者可以協同工作,以擴展可用選項的範圍。兩者都設計為彼此連續且一致,它們並排可用,並且來自每一方的回饋都有益於雙方。下圖顯示了兩者之間的關係、它們的共同點以及每個獨特支援的內容

spring mvc and webflux venn

我們建議您考慮以下具體要點

  • 如果您的 Spring MVC 應用程式運作良好,則無需變更。命令式程式設計是編寫、理解和偵錯程式碼的最簡單方法。您擁有最大的程式庫選擇,因為從歷史上看,大多數程式庫都是阻塞式的。

  • 如果您已經在尋找非阻塞式 Web Stack,Spring WebFlux 提供了與此領域中其他產品相同的執行模型優勢,並且還提供了伺服器 (Netty、Tomcat、Jetty、Undertow 和 Servlet 容器) 的選擇、程式設計模型 (註解式控制器和函數式 Web 端點) 的選擇以及反應式程式庫 (Reactor、RxJava 或其他) 的選擇。

  • 如果您對輕量級、函數式 Web Framework 感興趣,以便與 Java 8 Lambda 或 Kotlin 一起使用,則可以使用 Spring WebFlux 函數式 Web 端點。對於需求較不複雜、可以從更高的透明度和控制中受益的較小應用程式或微服務來說,這也是一個不錯的選擇。

  • 在微服務架構中,您可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器或 Spring WebFlux 函數式端點的應用程式。在兩個 Framework 中都支援相同的基於註解的程式設計模型,使得重用知識變得更容易,同時也為正確的工作選擇正確的工具。

  • 評估應用程式的一種簡單方法是檢查其依賴項。如果您有阻塞式持久性 API (JPA、JDBC) 或網路 API 要使用,則 Spring MVC 至少是常見架構的最佳選擇。在 Reactor 和 RxJava 中執行單獨執行緒上的阻塞式呼叫在技術上是可行的,但您不會充分利用非阻塞式 Web Stack。

  • 如果您有一個 Spring MVC 應用程式,其中包含對遠端服務的呼叫,請嘗試反應式 WebClient。您可以直接從 Spring MVC 控制器方法傳回反應式類型 (Reactor、RxJava、或其他)。每次呼叫的延遲或呼叫之間的相互依賴性越大,優勢就越顯著。Spring MVC 控制器也可以呼叫其他反應式元件。

  • 如果您有一個大型團隊,請記住轉向非阻塞式、函數式和宣告式程式設計的陡峭學習曲線。在不完全切換的情況下開始的實用方法是使用反應式 WebClient。除此之外,從小處著手並衡量優勢。我們預計,對於廣泛的應用程式來說,這種轉變是不必要的。如果您不確定要尋找哪些優勢,請首先了解非阻塞式 I/O 的運作方式 (例如,單執行緒 Node.js 上的並行) 及其影響。

伺服器

Spring WebFlux 在 Tomcat、Jetty、Servlet 容器以及非 Servlet 執行期 (如 Netty 和 Undertow) 上受到支援。所有伺服器都改編為低階 通用 API,以便跨伺服器支援更高階的 程式設計模型

Spring WebFlux 沒有內建支援來啟動或停止伺服器。但是,從 Spring 組態和 WebFlux 基礎架構 組裝應用程式並使用幾行程式碼 執行它 非常容易。

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 應用程式的 Context 中對應 Servlet 篩選器或直接操作 Servlet API。由於上述原因,在同一個 Context 中混合阻塞式 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 策略。

  • 資料存取程式庫和其他第三方依賴項也可以建立和使用它們自己的執行緒。

配置

Spring Framework 不支援啟動和停止 伺服器。若要配置伺服器的執行緒模型,您需要使用伺服器特定的配置 API,或者,如果您使用 Spring Boot,請檢查每個伺服器的 Spring Boot 配置選項。您可以直接配置 WebClient。對於所有其他程式庫,請參閱它們各自的文件。