Статьи

Spring Web-Flux — функциональный стиль с бэкэндом Cassandra

В предыдущем посте я рассмотрел основы Spring Web-Flux, которые обозначают реактивную поддержку в веб-слое среды Spring.

Я продемонстрировал сквозной пример с использованием Spring Data Cassandra и использованием традиционной поддержки аннотаций в Spring Web Layers, а именно:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
...
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, за исключением типов возврата, вместо того, чтобы возвращать типы доменов, эти конечные точки возвращают тип Publisher посредством реализаций Mono и Flux в ядре реактора и Spring-Web обрабатывают потоковую передачу контента обратно.

В этой статье я расскажу о другом способе представления конечных точек — использовании функционального стиля вместо стиля аннотаций. Позвольте мне признать, что я нашел статью Baeldung и пост Россена Стоянчева неоценимыми в моем понимании функционального стиля разоблачения конечных точек сети.

Отображение аннотаций на маршруты

Позвольте мне начать с нескольких конечных точек на основе аннотаций: одна для извлечения сущности, а другая для сохранения сущности:

01
02
03
04
05
06
07
08
09
10
@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 , и они могут составляться для создания всех конечных точек приложения по следующим направлениям:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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)
                ));
    }
}

Существуют вспомогательные функции (гнездо, маршрут, GET, принять и т. Д.), Которые упрощают создание всех функций Router вместе. Как только соответствующая функция RouterFunction найдена, запрос обрабатывается функцией HandlerFunction, которая в приведенном выше примере абстрагируется от HotelHandler, а функция сохранения и получения выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)
        );
    }
 
    ...
}

Вот как выглядит полная RouterFunction для всех API, поддерживаемых исходным проектом на основе аннотаций:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 и использовал предоставленные им утверждения для проверки поведения.

01
02
03
04
05
06
07
08
09
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
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, который может быть легче следовать, чем код в этом посте.

Ссылка: Spring Web-Flux — функциональный стиль с Cassandra Backend от нашего партнера JCG Биджу Кунджуммена в блоге all and sundry.