1. 引言
Spring WebFlux 是一個基於響應式原則的新型函數式 Web 框架。
在本教程中,我們將學習如何在實踐中與它一起工作。
我們將基於我們對 Spring 5 WebFlux 的現有指南。在該指南中,我們使用標註式組件創建了一個簡單的響應式 REST 應用程序。在這裏,我們將使用函數式框架。
2. Maven 依賴
我們需要與上一篇文章中相同的 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux">spring-boot-starter-webflux</a> 依賴項。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.3.2</version>
</dependency>3. 功能型 Web 框架
功能型 Web 框架引入了一種新的編程模型,其中我們使用函數來路由和處理請求。
與基於註解的模型(其中我們使用註解映射)不同,我們將使用 HandlerFunction 和 RouterFunctions
類似於註解控制器,功能性端點方法也建立在相同的響應式堆棧之上。
3.1. HandlerFunction
The HandlerFunction represents a function that generates responses for requests routed to them:
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}此接口主要是一個 Function<Request, Response<T>>, 其行為類似於 Servlet。
儘管如此,與標準 Servlet#service(ServletRequest req, ServletResponse res) 相比,HandlerFunction 不接受響應作為輸入參數。
3.2. 路由函數 (RouterFunction)
路由函數 (RouterFunction) 是@RequestMapping 註解的替代方案。我們可以使用它來將請求路由到處理函數:
@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
Mono<HandlerFunction<T>> route(ServerRequest request);
// ...
}通常,我們可以導入 RouterFunctions.route() 函數來創建路由,而不是編寫完整的路由函數。
它允許我們通過應用 RequestPredicate 來路由請求。當謂詞匹配時,則返回第二個參數,即處理函數:
public static <T extends ServerResponse> RouterFunction<T> route(
RequestPredicate predicate,
HandlerFunction<T> handlerFunction)因為 route() 方法返回一個 RouterFunction,我們可以將其鏈接起來構建強大的、複雜的路由方案。
4. 使用函數式 Web 構建反應式 REST 應用程序
在之前的指南中,我們使用 @RestController 和 WebClient 創建了一個簡單的 EmployeeManagement REST 應用程序。
現在,讓我們使用路由器和處理函數來實現相同的邏輯。
首先,我們需要使用 RouterFunction 創建路由,以便發佈和消費流式 Employee 對象。
路由註冊為 Spring Bean,並且可以在任何配置類中創建。
4.1. 單個資源
讓我們使用 RouterFunction 創建我們的第一個路由,發佈一個單個 Employee 資源:
@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
return route(GET("/employees/{id}"),
req -> ok().body(
employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
}第一個參數是一個請求謂詞。請注意,我們這裏使用了靜態導入的 RequestPredicates.GET 方法。第二個參數定義了一個處理函數,該函數將在謂詞應用於時使用。
換句話説,上面的示例將所有對 /employees/{id} 的 GET 請求路由到 EmployeeRepository#findEmployeeById(String id) 方法。
4.2. 資源集合
接下來,為了發佈資源集合,我們添加另一個路由:
@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
return route(GET("/employees"),
req -> ok().body(
employeeRepository().findAllEmployees(), Employee.class));
}4.3. 單個資源更新
最後,讓我們添加一個用於更新 Employee 資源的路由:
@Bean
RouterFunction<ServerResponse> updateEmployeeRoute() {
return route(POST("/employees/update"),
req -> req.body(toMono(Employee.class))
.doOnNext(employeeRepository()::updateEmployee)
.then(ok().build()));
}5. 路由組合
我們也可以在一個單獨的路由器函數中將路由組合起來。
以下是如何組合上述創建的路由:
@Bean
RouterFunction<ServerResponse> composedRoutes() {
return
route(GET("/employees"),
req -> ok().body(
employeeRepository().findAllEmployees(), Employee.class))
.and(route(GET("/employees/{id}"),
req -> ok().body(
employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class)))
.and(route(POST("/employees/update"),
req -> req.body(toMono(Employee.class))
.doOnNext(employeeRepository()::updateEmployee)
.then(ok().build())));
}在這裏,我們使用了 RouterFunction.and() 將我們的路由組合在一起。
最後,我們使用路由器和處理程序實現了用於我們 EmployeeManagement 應用程序的完整 REST API。
要運行應用程序,我們可以使用單獨的路由或我們上面創建的單個、組合的路由。
6. 測試路由
我們可以使用 WebTestClient 來測試我們的路由。
要做到這一點,我們首先需要使用 bindToRouterFunction 方法綁定路由,然後構建測試客户端實例。
讓我們測試我們的 getEmployeeByIdRoute :
@Test
void givenEmployeeId_whenGetEmployeeById_thenCorrectEmployee() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.getEmployeeByIdRoute())
.build();
Employee employee = new Employee("1", "Employee 1");
given(employeeRepository.findEmployeeById("1")).willReturn(Mono.just(employee));
client.get()
.uri("/employees/1")
.exchange()
.expectStatus()
.isOk()
.expectBody(Employee.class)
.isEqualTo(employee);
}並且同樣,getAllEmployeesRoute:
@Test
void whenGetAllEmployees_thenCorrectEmployees() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.getAllEmployeesRoute())
.build();
List<Employee> employees = Arrays.asList(
new Employee("1", "Employee 1"),
new Employee("2", "Employee 2"));
Flux<Employee> employeeFlux = Flux.fromIterable(employees);
given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);
client.get()
.uri("/employees")
.exchange()
.expectStatus()
.isOk()
.expectBodyList(Employee.class)
.isEqualTo(employees);
}我們還可以通過斷言我們的 updateEmployeeRoute 方法通過 EmployeeRepository 更新我們的 Employee 實例來測試它:
@Test
void whenUpdateEmployee_thenEmployeeUpdated() {
WebTestClient client = WebTestClient
.bindToRouterFunction(config.updateEmployeeRoute())
.build();
Employee employee = new Employee("1", "Employee 1 Updated");
client.post()
.uri("/employees/update")
.body(Mono.just(employee), Employee.class)
.exchange()
.expectStatus()
.isOk();
verify(employeeRepository).updateEmployee(employee);
}有關使用 WebTestClient 進行測試的更多詳細信息,請參閲我們關於使用 WebClient 和 WebTestClient 的教程。
7. 總結
本教程介紹了 Spring 5 中的新型函數式 Web 框架,並深入研究了其兩個核心接口:<em RouterFunction</em> 和 <em HandlerFunction</em>。我們還學習瞭如何創建各種路由來處理請求併發送響應。
此外,我們使用函數式端點模型,重構了在 Spring 5 WebFlux 指南中介紹的 <em EmployeeManagement</em> 應用程序。