在 上一篇文章 中,我介绍了 Spring Web-Flux 的基础知识,它表示 Spring 框架的 Web 层中的 reactive 支持。
我已经使用 Spring Data Cassandra 并使用 Spring Web Layers 中的传统注释支持演示了一个端到端示例,大致如下:
... import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; ... @RestController @RequestMapping("/hotels") public class HotelController { @GetMapping(path = "/{id}") public Mono<Hotel> get(@PathVariable("id") UUID uuid) { ... } @GetMapping(path = "/startingwith/{letter}") public Flux<HotelByLetter> findHotelsWithLetter( @PathVariable("letter") String letter) { ... } }
这看起来像传统的 Spring Web 注释,除了返回类型,而不是返回域类型,这些端点通过 reactor-corePublisher 类型span> 和 Spring-Web 处理流回的内容。
在这篇文章中,我将介绍一种不同的公开端点的方式——使用函数式风格而不是注释风格。我承认,我发现 Baeldung 的文章 和 Rossen Stoyanchev 的帖子 对我理解公开 Web 端点的功能风格非常宝贵。
让我从一些基于注释的端点开始,一个用于检索实体,一个用于保存实体:
@GetMapping(path = "/{id}") public Mono<Hotel> get(@PathVariable("id") UUID uuid) { return this.hotelService.findOne(uuid); } @PostMapping public Mono<ResponseEntity<Hotel>> save(@RequestBody Hotel hotel) { return this.hotelService.save(hotel) .map(savedHotel -> new ResponseEntity<>(savedHotel, HttpStatus.CREATED)); }
在公开端点的功能样式中,每个端点都将转换为 RouterFunction,并且它们可以组合以创建应用程序的所有端点,如下所示:
package cass.web; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RouterFunction; import static org.springframework.web.reactive.function.server.RequestPredicates.*; import static org.springframework.web.reactive.function.server.RouterFunctions.*; public interface ApplicationRoutes { static RouterFunction<?> routes(HotelHandler hotelHandler) { return nest(path("/hotels"), nest(accept(MediaType.APPLICATION_JSON), route(GET("/{id}"), hotelHandler::get) .andRoute(POST("/"), hotelHandler::save) )); } }
有一些辅助函数(nest、route、GET、accept 等)可以轻而易举地将所有 RouterFunction 组合在一起。一旦找到合适的 RouterFunction,请求将由 HandlerFunction 处理,在上面的示例中,它由 HotelHandler 抽象,保存和获取功能如下所示:
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.UUID; @Service public class HotelHandler { ... public Mono<ServerResponse> get(ServerRequest request) { UUID uuid = UUID.fromString(request.pathVariable("id")); Mono<ServerResponse> notFound = ServerResponse.notFound().build(); return this.hotelService.findOne(uuid) .flatMap(hotel -> ServerResponse.ok().body(Mono.just(hotel), Hotel.class)) .switchIfEmpty(notFound); } public Mono<ServerResponse> save(ServerRequest serverRequest) { Mono<Hotel> hotelToBeCreated = serverRequest.bodyToMono(Hotel.class); return hotelToBeCreated.flatMap(hotel -> ServerResponse.status(HttpStatus.CREATED).body(hotelService.save(hotel), Hotel.class) ); } ... }
这是原始基于注释的项目支持的所有 API 的完整 RouterFunction 的样子:
import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RouterFunction; import static org.springframework.web.reactive.function.server.RequestPredicates.*; import static org.springframework.web.reactive.function.server.RouterFunctions.*; public interface ApplicationRoutes { static RouterFunction<?> routes(HotelHandler hotelHandler) { return nest(path("/hotels"), nest(accept(MediaType.APPLICATION_JSON), route(GET("/{id}"), hotelHandler::get) .andRoute(POST("/"), hotelHandler::save) .andRoute(PUT("/"), hotelHandler::update) .andRoute(DELETE("/{id}"), hotelHandler::delete) .andRoute(GET("/startingwith/{letter}"), hotelHandler::findHotelsWithLetter) .andRoute(GET("/fromstate/{state}"), hotelHandler::findHotelsInState) )); } }
测试这些路由也很容易,Spring Webflux 提供了一个 WebTestClient 来测试路由,同时提供模拟其背后实现的能力
例如,要测试 get by id 端点,我会将 WebTestClient 绑定到之前定义的 RouterFunction 并使用它提供的断言来测试行为。
import org.junit.Before; import org.junit.Test; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import java.util.UUID; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class GetRouteTests { private WebTestClient client; private HotelService hotelService; private UUID sampleUUID = UUID.fromString("fd28ec06-6de5-4f68-9353-59793a5bdec2"); @Before public void setUp() { this.hotelService = mock(HotelService.class); when(hotelService.findOne(sampleUUID)).thenReturn(Mono.just(new Hotel(sampleUUID, "test"))); HotelHandler hotelHandler = new HotelHandler(hotelService); this.client = WebTestClient.bindToRouterFunction(ApplicationRoutes.routes(hotelHandler)).build(); } @Test public void testHotelGet() throws Exception { this.client.get().uri("/hotels/" + sampleUUID) .exchange() .expectStatus().isOk() .expectBody(Hotel.class) .isEqualTo(new Hotel(sampleUUID, "test")); } }
定义路由的函数式方式绝对与基于注释的方式截然不同——我喜欢它是一种更明确的定义端点的方式,以及如何处理对端点的调用,注释总是感觉多一点神奇。
我的 github 存储库中有一个完整的工作代码,它可能比本文中的代码更容易理解。
标签2: Java教程地址:https://www.cundage.com/article/jcg-spring-web-flux-functional-style-cassandra-backend.html