使用 Spring 5 的 WebFlux 開發反應式 Web 應用
Spring 5 是流行的 Spring 框架的下一個重大的版本升級。Spring 5 中最重要改動是把反應式程式設計的思想應用到了框架的各個方面,Spring 5 的反應式程式設計以 Reactor 庫為基礎。在之前的文章《使用 Reactor 進行反應式程式設計》中,已經對 Reactor 庫進行了詳細的介紹。讀者如果需要了解 Reactor,可以參考之前的那篇文章。Spring 5 框架所包含的內容很多,本文只重點介紹其中新增的 WebFlux 模組。開發人員可以使用 WebFlux 建立高效能的 Web 應用和客戶端。本文對 WebFlux 模組進行了詳細介紹,包括其中的 HTTP、伺服器推送事件和 WebSocket 支援。
WebFlux 簡介
WebFlux 模組的名稱是 spring-webflux,名稱中的 Flux 來源於 Reactor 中的類 Flux。該模組中包含了對反應式 HTTP、伺服器推送事件和 WebSocket 的客戶端和伺服器端的支援。對於開發人員來說,比較重要的是伺服器端的開發,這也是本文的重點。在伺服器端,WebFlux 支援兩種不同的程式設計模型:第一種是 Spring MVC 中使用的基於 Java 註解的方式;第二種是基於 Java 8 的 lambda 表示式的函數語言程式設計模型。這兩種程式設計模型只是在程式碼編寫方式上存在不同。它們執行在同樣的反應式底層架構之上,因此在執行時是相同的。WebFlux 需要底層提供執行時的支援,WebFlux 可以執行在支援 Servlet 3.1 非阻塞 IO API 的 Servlet 容器上,或是其他非同步執行時環境,如 Netty 和 Undertow。
最方便的建立 WebFlux 應用的方式是使用 Spring Boot 提供的應用模板。直接訪問 Spring Initializ 網站(http://start.spring.io/),選擇建立一個 Maven 或 Gradle 專案。Spring Boot 的版本選擇 2.0.0 M2。在新增的依賴中,選擇 Reactive Web。最後輸入應用所在的分組和名稱,點選進行下載即可。需要注意的是,只有在選擇了 Spring Boot 2.0.0 M2 之後,依賴中才可以選擇 Reactive Web。下載完成之後可以匯入到 IDE 中進行編輯。本文的示例程式碼使用 Intellij IDEA 2017.2 進行編寫。
本文從三個方面對 WebFlux 進行介紹。首先是使用經典的基於 Java 註解的程式設計模型來進行開發,其次是使用 WebFlux 新增的函數語言程式設計模型來進行開發,最後介紹 WebFlux 應用的測試。通過這樣循序漸進的方式讓讀者瞭解 WebFlux 應用開發的細節。
Java 註解程式設計模型
基於 Java 註解的程式設計模型,對於使用過 Spring MVC 的開發人員來說是再熟悉不過的。在 WebFlux 應用中使用同樣的模式,容易理解和上手。我們先從最經典的 Hello World 的示例開始說明。程式碼清單 1 中的 BasicController 是 REST API 的控制器,通過@RestController 註解來宣告。在 BasicController 中聲明瞭一個 URI 為/hello_world 的對映。其對應的方法 sayHelloWorld()的返回值是 Mono<String>型別,其中包含的字串"Hello World"會作為 HTTP 的響應內容。
清單 1. Hello World 示例
1 2 3 4 5 6 7 |
|
從程式碼清單 1 中可以看到,使用 WebFlux 與 Spring MVC 的不同在於,WebFlux 所使用的型別是與反應式程式設計相關的 Flux 和 Mono 等,而不是簡單的物件。對於簡單的 Hello World 示例來說,這兩者之間並沒有什麼太大的差別。對於複雜的應用來說,反應式程式設計和負壓的優勢會體現出來,可以帶來整體的效能的提升。
REST API
簡單的 Hello World 示例並不足以說明 WebFlux 的用法。在下面的小節中,本文將介紹其他具體的例項。先從 REST API 開始說起。REST API 在 Web 伺服器端應用中佔據了很大的一部分。我們通過一個具體的例項來說明如何使用 WebFlux 來開發 REST API。
該 REST API 用來對使用者資料進行基本的 CRUD 操作。作為領域物件的 User 類中包含了 id、name 和 email 等三個基本的屬性。為了對 User 類進行操作,我們需要提供服務類 UserService,如程式碼清單 2 所示。類 UserService 使用一個 Map 來儲存所有使用者的資訊,並不是一個持久化的實現。這對於示例應用來說已經足夠了。類 UserService 中的方法都以 Flux 或 Mono 物件作為返回值,這也是 WebFlux 應用的特徵。在方法 getById()中,如果找不到 ID 對應的 User 物件,會返回一個包含了 ResourceNotFoundException 異常通知的 Mono 物件。方法 getById()和 createOrUpdate()都可以接受 String 或 Flux 型別的引數。Flux 型別的引數表示的是有多個物件需要處理。這裡使用 doOnNext()來對其中的每個物件進行處理。
清單 2. UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
程式碼清單 3 中的類 UserController 是具體的 Spring MVC 控制器類。它使用類 UserService 來完成具體的功能。類 UserController 中使用了註解@ExceptionHandler 來添加了 ResourceNotFoundException 異常的處理方法,並返回 404 錯誤。類 UserController 中的方法都很簡單,只是簡單地代理給 UserService 中的對應方法。
清單 3. UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
伺服器推送事件
伺服器推送事件(Server-Sent Events,SSE)允許伺服器端不斷地推送資料到客戶端。相對於 WebSocket 而言,伺服器推送事件只支援伺服器端到客戶端的單向資料傳遞。雖然功能較弱,但優勢在於 SSE 在已有的 HTTP 協議上使用簡單易懂的文字格式來表示傳輸的資料。作為 W3C 的推薦規範,SSE 在瀏覽器端的支援也比較廣泛,除了 IE 之外的其他瀏覽器都提供了支援。在 IE 上也可以使用 polyfill 庫來提供支援。在伺服器端來說,SSE 是一個不斷產生新資料的流,非常適合於用反應式流來表示。在 WebFlux 中建立 SSE 的伺服器端是非常簡單的。只需要返回的物件的型別是 Flux<ServerSentEvent>,就會被自動按照 SSE 規範要求的格式來發送響應。
程式碼清單 4 中的 SseController 是一個使用 SSE 的控制器的示例。其中的方法 randomNumbers()表示的是每隔一秒產生一個隨機數的 SSE 端點。我們可以使用類 ServerSentEvent.Builder 來建立 ServerSentEvent 物件。這裡我們指定了事件名稱 random,以及每個事件的識別符號和資料。事件的識別符號是一個遞增的整數,而資料則是產生的隨機數。
清單 4. 伺服器推送事件示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
在測試 SSE 時,我們只需要使用 curl 來訪問即可。程式碼清單 5 給出了呼叫 curl http://localhost:8080/sse/randomNumbers 的結果。
清單 5. SSE 伺服器端傳送的響應
1 2 3 4 5 6 7 8 9 10 11 |
|
WebSocket
WebSocket 支援客戶端與伺服器端的雙向通訊。當客戶端與伺服器端之間的互動方式比較複雜時,可以使用 WebSocket。WebSocket 在主流的瀏覽器上都得到了支援。WebFlux 也對建立 WebSocket 伺服器端提供了支援。在伺服器端,我們需要實現介面 org.springframework.web.reactive.socket.WebSocketHandler 來處理 WebSocket 通訊。介面 WebSocketHandler 的方法 handle 的引數是介面 WebSocketSession 的物件,可以用來獲取客戶端資訊、接送訊息和傳送訊息。程式碼清單 6 中的 EchoHandler 對於每個接收的訊息,會發送一個添加了"ECHO -> "字首的響應訊息。WebSocketSession 的 receive 方法的返回值是一個 Flux<WebSocketMessage>物件,表示的是接收到的訊息流。而 send 方法的引數是一個 Publisher<WebSocketMessage>物件,表示要傳送的訊息流。在 handle 方法,使用 map 操作對 receive 方法得到的 Flux<WebSocketMessage>中包含的訊息繼續處理,然後直接由 send 方法來發送。
清單 6. WebSocket 的 EchoHandler 示例
1 2 3 4 5 6 7 8 9 |
|
在建立了 WebSocket 的處理器 EchoHandler 之後,下一步需要把它註冊到 WebFlux 中。我們首先需要建立一個類 WebSocketHandlerAdapter 的物件,該物件負責把 WebSocketHandler 關聯到 WebFlux 中。程式碼清單 7 中給出了相應的 Spring 配置。其中的 HandlerMapping 型別的 bean 把 EchoHandler 對映到路徑 /echo。
清單 7. 註冊 EchoHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
執行應用之後,可以使用工具來測試該 WebSocket 服務。開啟工具頁面 https://www.websocket.org/echo.html,然後連線到 ws://localhost:8080/echo,可以傳送訊息並檢視伺服器端返回的結果。
函數語言程式設計模型
在上節中介紹了基於 Java 註解的程式設計模型,WebFlux 還支援基於 lambda 表示式的函數語言程式設計模型。與基於 Java 註解的程式設計模型相比,函數語言程式設計模型的抽象層次更低,程式碼編寫更靈活,可以滿足一些對動態性要求更高的場景。不過在編寫時的程式碼複雜度也較高,學習曲線也較陡。開發人員可以根據實際的需要來選擇合適的程式設計模型。目前 Spring Boot 不支援在一個應用中同時使用兩種不同的程式設計模式。
為了說明函數語言程式設計模型的用法,我們使用 Spring Initializ 來建立一個新的 WebFlux 專案。在函數語言程式設計模型中,每個請求是由一個函式來處理的, 通過介面 org.springframework.web.reactive.function.server.HandlerFunction 來表示。HandlerFunction 是一個函式式介面,其中只有一個方法 Mono<T extends ServerResponse> handle(ServerRequest request),因此可以用 labmda 表示式來實現該介面。介面 ServerRequest 表示的是一個 HTTP 請求。通過該介面可以獲取到請求的相關資訊,如請求路徑、HTTP 頭、查詢引數和請求內容等。方法 handle 的返回值是一個 Mono<T extends ServerResponse>物件。介面 ServerResponse 用來表示 HTTP 響應。ServerResponse 中包含了很多靜態方法來建立不同 HTTP 狀態碼的響應物件。本節中通過一個簡單的計算器來展示函數語言程式設計模型的用法。程式碼清單 8 中給出了處理不同請求的類 CalculatorHandler,其中包含的方法 add、subtract、multiply 和 divide 都是介面 HandlerFunction 的實現。這些方法分別對應加、減、乘、除四種運算。每種運算都是從 HTTP 請求中獲取到兩個作為運算元的整數,再把運算的結果返回。
清單 8. 處理請求的類 CalculatorHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
在建立了處理請求的 HandlerFunction 之後,下一步是為這些 HandlerFunction 提供路由資訊,也就是這些 HandlerFunction 被呼叫的條件。這是通過函式式介面 org.springframework.web.reactive.function.server.RouterFunction 來完成的。介面 RouterFunction 的方法 Mono<HandlerFunction<T extends ServerResponse>> route(ServerRequest request)對每個 ServerRequest,都返回對應的 0 個或 1 個 HandlerFunction 物件,以 Mono<HandlerFunction>來表示。當找到對應的 HandlerFunction 時,該 HandlerFunction 被呼叫來處理該 ServerRequest,並把得到的 ServerResponse 返回。在使用 WebFlux 的 Spring Boot 應用中,只需要建立 RouterFunction 型別的 bean,就會被自動註冊來處理請求並呼叫相應的 HandlerFunction。
程式碼清單 9 給了示例相關的配置類 Config。方法 RouterFunctions.route 用來根據 Predicate 是否匹配來確定 HandlerFunction 是否被應用。RequestPredicates 中包含了很多靜態方法來建立常用的基於不同匹配規則的 Predicate。如 RequestPredicates.path 用來根據 HTTP 請求的路徑來進行匹配。此處我們檢查請求的路徑是/calculator。在清單 9 中,我們首先使用 ServerRequest 的 queryParam 方法來獲取到查詢引數 operator 的值,然後通過反射 API 在類 CalculatorHandler 中找到與查詢引數 operator 的值名稱相同的方法來確定要呼叫的 HandlerFunction 的實現,最後呼叫查詢到的方法來處理該請求。如果找不到查詢引數 operator 或是 operator 的值不在識別的列表中,伺服器端返回 400 錯誤;如果反射 API 的方法呼叫中出現錯誤,伺服器端返回 500 錯誤。
清單 9. 註冊 RouterFunction
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
客戶端
除了伺服器端實現之外,WebFlux 也提供了反應式客戶端,可以訪問 HTTP、SSE 和 WebSocket 伺服器端。
HTTP
對於 HTTP 和 SSE,可以使用 WebFlux 模組中的類 org.springframework.web.reactive.function.client.WebClient。程式碼清單 10 中的 RESTClient 用來訪問前面小節中建立的 REST API。首先使用 WebClient.create 方法來建立一個新的 WebClient 物件,然後使用方法 post 來建立一個 POST 請求,並使用方法 body 來設定 POST 請求的內容。方法 exchange 的作用是傳送請求並得到以 Mono<ServerResponse>表示的 HTTP 響應。最後對得到的響應進行處理並輸出結果。ServerResponse 的 bodyToMono 方法把響應內容轉換成類 User 的物件,最終得到的結果是 Mono<User>物件。呼叫 createdUser.block 方法的作用是等待請求完成並得到所產生的類 User 的物件。
清單 10. 使用 WebClient 訪問 REST API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
SSE
WebClient 還可以用同樣的方式來訪問 SSE 服務,如程式碼清單 11 所示。這裡我們訪問的是在之前的小節中建立的生成隨機數的 SSE 服務。使用 WebClient 訪問 SSE 在傳送請求部分與訪問 REST API 是相同的,所不同的地方在於對 HTTP 響應的處理。由於 SSE 服務的響應是一個訊息流,我們需要使用 flatMapMany 把 Mono<ServerResponse>轉換成一個 Flux<ServerSentEvent>物件,這是通過方法 BodyExtractors.toFlux 來完成的,其中的引數 new ParameterizedTypeReference<ServerSentEvent<String>>() {}表明了響應訊息流中的內容是 ServerSentEvent 物件。由於 SSE 伺服器會不斷地傳送訊息,這裡我們只是通過 buffer 方法來獲取前 10 條訊息並輸出。
清單 11. 使用 WebClient 訪問 SSE 服務
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
WebSocket
訪問 WebSocket 不能使用 WebClient,而應該使用專門的 WebSocketClient 客戶端。Spring Boot 的 WebFlux 模板中預設使用的是 Reactor Netty 庫。Reactor Netty 庫提供了 WebSocketClient 的實現。在程式碼清單 12 中,我們訪問的是上面小節中建立的 WebSocket 服務。WebSocketClient 的 execute 方法與 WebSocket 伺服器建立連線,並執行給定的 WebSocketHandler 物件。該 WebSocketHandler 物件與程式碼清單 6 中的作用是一樣的,只不過它是工作於客戶端,而不是伺服器端。在 WebSocketHandler 的實現中,首先通過 WebSocketSession 的 send 方法來發送字串 Hello 到伺服器端,然後通過 receive 方法來等待伺服器端的響應並輸出。方法 take(1)的作用是表明客戶端只獲取伺服器端傳送的第一條訊息。
清單 12. 使用 WebSocketClient 訪問 WebSocket
1 2 3 4 5 6 7 8 9 10 11 |
|
測試
在 spring-test 模組中也添加了對 WebFlux 的支援。通過類 org.springframework.test.web.reactive.server.WebTestClient 可以測試 WebFlux 伺服器。進行測試時既可以通過 mock 的方式來進行,也可以對實際執行的伺服器進行整合測試。程式碼清單 13 通過一個整合測試來測試 UserController 中的建立使用者的功能。方法 WebTestClient.bindToServer 繫結到一個執行的伺服器並設定了基礎 URL。傳送 HTTP 請求的方式與程式碼清單 10 相同,不同的是 exchange 方法的返回值是 ResponseSpec 物件,其中包含了 expectStatus 和 expectBody 等方法來驗證 HTTP 響應的狀態碼和內容。方法 jsonPath 可以根據 JSON 物件中的路徑來進行驗證。
清單 13. 測試 UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
小結
反應式程式設計正規化為開發高效能 Web 應用帶來了新的機會和挑戰。Spring 5 中的 WebFlux 模組可以作為開發反應式 Web 應用的基礎。由於 Spring 框架的流行,WebFlux 會成為開發 Web 應用的重要趨勢之一。本文對 Spring 5 中的 WebFlux 模組進行了詳細的介紹,包括如何用 WebFlux 開發 HTTP、SSE 和 WebSocket 伺服器端應用,以及作為客戶端來訪問 HTTP、SSE 和 WebSocket 服務。對於 WebFlux 的基於 Java 註解和函數語言程式設計等兩種模型都進行了介紹。最後介紹瞭如何測試 WebFlux 應用。
參考資源 (resources)
- 參考 WebFlux 的參考指南,瞭解 WebFlux 的更多內容。
- 檢視《使用 Reactor 進行反應式程式設計》一文,瞭解 Reactor 專案的更多內容。