전체 페이지뷰

2017년 4월 25일 화요일

spring web flux

https://www.javacodegeeks.com/2017/04/spring-web-flux-functional-style-cassandra-backend.html


Spring Web-Flux – Functional Style with Cassandra Backend

In a previous post I had walked through the basics of Spring Web-Flux which denotes the reactive support in the web layer of Spring framework.
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:
01...
02import org.springframework.web.bind.annotation.*;
03import reactor.core.publisher.Flux;
04import reactor.core.publisher.Mono;
05...
06 
07@RestController
08@RequestMapping("/hotels")
09public class HotelController {
10 
11    @GetMapping(path = "/{id}")
12    public Mono<Hotel> get(@PathVariable("id") UUID uuid) {
13        ...
14    }
15 
16    @GetMapping(path = "/startingwith/{letter}")
17    public Flux<HotelByLetter> findHotelsWithLetter(
18            @PathVariable("letter") String letter) {
19        ...
20    }
21 
22}
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}")
02public Mono<Hotel> get(@PathVariable("id") UUID uuid) {
03    return this.hotelService.findOne(uuid);
04}
05 
06@PostMapping
07public Mono<ResponseEntity<Hotel>> save(@RequestBody Hotel hotel) {
08    return this.hotelService.save(hotel)
09            .map(savedHotel -> new ResponseEntity<>(savedHotel, HttpStatus.CREATED));
10}
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:
01package cass.web;
02 
03import org.springframework.http.MediaType;
04import org.springframework.web.reactive.function.server.RouterFunction;
05 
06import static org.springframework.web.reactive.function.server.RequestPredicates.*;
07import static org.springframework.web.reactive.function.server.RouterFunctions.*;
08 
09public 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)
15                ));
16    }
17}
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:
01import org.springframework.web.reactive.function.server.ServerRequest;
02import org.springframework.web.reactive.function.server.ServerResponse;
03import reactor.core.publisher.Flux;
04import reactor.core.publisher.Mono;
05 
06import java.util.UUID;
07 
08@Service
09public class HotelHandler {
10 
11    ...
12     
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);
19    }
20 
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)
25        );
26    }
27 
28    ...
29}
This is how a complete RouterFunction for all the API’s supported by the original annotation based project looks like:
01import org.springframework.http.MediaType;
02import org.springframework.web.reactive.function.server.RouterFunction;
03 
04import static org.springframework.web.reactive.function.server.RequestPredicates.*;
05import static org.springframework.web.reactive.function.server.RouterFunctions.*;
06 
07public 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)
17                ));
18    }
19}

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.
01import org.junit.Before;
02import org.junit.Test;
03import org.springframework.test.web.reactive.server.WebTestClient;
04import reactor.core.publisher.Mono;
05 
06import java.util.UUID;
07 
08import static org.mockito.Mockito.mock;
09import static org.mockito.Mockito.when;
10 
11 
12public class GetRouteTests {
13 
14    private WebTestClient client;
15    private HotelService hotelService;
16 
17    private UUID sampleUUID = UUID.fromString("fd28ec06-6de5-4f68-9353-59793a5bdec2");
18 
19    @Before
20    public void setUp() {
21        this.hotelService = mock(HotelService.class);
22        when(hotelService.findOne(sampleUUID)).thenReturn(Mono.just(new Hotel(sampleUUID, "test")));
23        HotelHandler hotelHandler = new HotelHandler(hotelService);
24         
25        this.client = WebTestClient.bindToRouterFunction(ApplicationRoutes.routes(hotelHandler)).build();
26    }
27 
28    @Test
29    public void testHotelGet() throws Exception {
30        this.client.get().uri("/hotels/" + sampleUUID)
31                .exchange()
32                .expectStatus().isOk()
33                .expectBody(Hotel.class)
34                .isEqualTo(new Hotel(sampleUUID, "test"));
35    }
36}

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.
I have a complete working code in my github repo which may be easier to follow than the code in this post.

댓글 없음:

댓글 쓰기