I had demonstrated an end to end sample using
Spring Data Cassandra and using the traditional annotations support in the Spring Web Layers, along these lines:
02 | import org.springframework.web.bind.annotation.*; |
03 | import reactor.core.publisher.Flux; |
04 | import reactor.core.publisher.Mono; |
08 | @RequestMapping ( "/hotels" ) |
09 | public class HotelController { |
11 | @GetMapping (path = "/{id}" ) |
12 | public Mono<Hotel> get( @PathVariable ( "id" ) UUID uuid) { |
16 | @GetMapping (path = "/startingwith/{letter}" ) |
17 | public Flux<HotelByLetter> findHotelsWithLetter( |
18 | @PathVariable ( "letter" ) String letter) { |
This looks like the traditional Spring Web annotations except for the return types, instead of returning the domain types these endpoints are returning the
Publisher type via the implementations of Mono and Flux in
reactor-core and Spring-Web handles streaming the content back.
In this post I will cover a different way of exposing the endpoints – using a functional style instead of the annotations style. Let me acknowledge that I have found
Baeldung’s article and
Rossen Stoyanchev’s post invaluable in my understanding of the functional style of exposing the web endpoints.
Mapping the annotations to routes
Let me start with a few annotation based endpoints, one to retrieve an entity and one to save an entity:
01 | @GetMapping (path = "/{id}" ) |
02 | public Mono<Hotel> get( @PathVariable ( "id" ) UUID uuid) { |
03 | return this .hotelService.findOne(uuid); |
07 | public Mono<ResponseEntity<Hotel>> save( @RequestBody Hotel hotel) { |
08 | return this .hotelService.save(hotel) |
09 | .map(savedHotel -> new ResponseEntity<>(savedHotel, HttpStatus.CREATED)); |
In a functional style of exposing the endpoints, each of the endpoints would translate to a
RouterFunction, and they can composed to create all the endpoints of the app, along these lines:
03 | import org.springframework.http.MediaType; |
04 | import org.springframework.web.reactive.function.server.RouterFunction; |
06 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; |
07 | import static org.springframework.web.reactive.function.server.RouterFunctions.*; |
09 | public interface ApplicationRoutes { |
10 | static RouterFunction<?> routes(HotelHandler hotelHandler) { |
11 | return nest(path( "/hotels" ), |
12 | nest(accept(MediaType.APPLICATION_JSON), |
13 | route(GET( "/{id}" ), hotelHandler::get) |
14 | .andRoute(POST( "/" ), hotelHandler::save) |
There are helper functions(nest, route, GET, accept etc) which make composing all the RouterFunction(s) together a breeze. Once an appropriate RouterFunction is found, the request is handled by a
HandlerFunction which in the above sample is abstracted by the HotelHandler and for the save and get functionality looks like this:
01 | import org.springframework.web.reactive.function.server.ServerRequest; |
02 | import org.springframework.web.reactive.function.server.ServerResponse; |
03 | import reactor.core.publisher.Flux; |
04 | import reactor.core.publisher.Mono; |
09 | public class HotelHandler { |
13 | public Mono<ServerResponse> get(ServerRequest request) { |
14 | UUID uuid = UUID.fromString(request.pathVariable( "id" )); |
15 | Mono<ServerResponse> notFound = ServerResponse.notFound().build(); |
16 | return this .hotelService.findOne(uuid) |
17 | .flatMap(hotel -> ServerResponse.ok().body(Mono.just(hotel), Hotel. class )) |
18 | .switchIfEmpty(notFound); |
21 | public Mono<ServerResponse> save(ServerRequest serverRequest) { |
22 | Mono<Hotel> hotelToBeCreated = serverRequest.bodyToMono(Hotel. class ); |
23 | return hotelToBeCreated.flatMap(hotel -> |
24 | ServerResponse.status(HttpStatus.CREATED).body(hotelService.save(hotel), Hotel. class ) |
This is how a complete RouterFunction for all the API’s supported by the original annotation based project looks like:
01 | import org.springframework.http.MediaType; |
02 | import org.springframework.web.reactive.function.server.RouterFunction; |
04 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; |
05 | import static org.springframework.web.reactive.function.server.RouterFunctions.*; |
07 | public interface ApplicationRoutes { |
08 | static RouterFunction<?> routes(HotelHandler hotelHandler) { |
09 | return nest(path( "/hotels" ), |
10 | nest(accept(MediaType.APPLICATION_JSON), |
11 | route(GET( "/{id}" ), hotelHandler::get) |
12 | .andRoute(POST( "/" ), hotelHandler::save) |
13 | .andRoute(PUT( "/" ), hotelHandler::update) |
14 | .andRoute(DELETE( "/{id}" ), hotelHandler::delete) |
15 | .andRoute(GET( "/startingwith/{letter}" ), hotelHandler::findHotelsWithLetter) |
16 | .andRoute(GET( "/fromstate/{state}" ), hotelHandler::findHotelsInState) |
Testing functional Routes
It is easy to test these routes also, Spring Webflux provides a
WebTestClient to test out the routes while providing the ability to mock the implementations behind it
For eg, to test the get by id endpoint, I would bind the WebTestClient to the RouterFunction defined before and use the assertions that it provides to test the behavior.
01 | import org.junit.Before; |
03 | import org.springframework.test.web.reactive.server.WebTestClient; |
04 | import reactor.core.publisher.Mono; |
08 | import static org.mockito.Mockito.mock; |
09 | import static org.mockito.Mockito.when; |
12 | public class GetRouteTests { |
14 | private WebTestClient client; |
15 | private HotelService hotelService; |
17 | private UUID sampleUUID = UUID.fromString( "fd28ec06-6de5-4f68-9353-59793a5bdec2" ); |
21 | this .hotelService = mock(HotelService. class ); |
22 | when(hotelService.findOne(sampleUUID)).thenReturn(Mono.just( new Hotel(sampleUUID, "test" ))); |
23 | HotelHandler hotelHandler = new HotelHandler(hotelService); |
25 | this .client = WebTestClient.bindToRouterFunction(ApplicationRoutes.routes(hotelHandler)).build(); |
29 | public void testHotelGet() throws Exception { |
30 | this .client.get().uri( "/hotels/" + sampleUUID) |
32 | .expectStatus().isOk() |
33 | .expectBody(Hotel. class ) |
34 | .isEqualTo( new Hotel(sampleUUID, "test" )); |
Conclusion
The functional way of defining the routes is definitely a very different approach from the annotation based one – I like that it is a far more explicit way of defining an endpoint and how the calls for the endpoint is handled, the annotations always felt a little more magical.
댓글 없음:
댓글 쓰기