1. Введение
Spring WebFlux — это новая функциональная веб-инфраструктура, построенная с использованием реактивных принципов.
В этом уроке мы научимся работать с ним на практике.
Мы будем основываться на нашем существующем руководстве по Spring 5 WebFlux . В этом руководстве мы создали простое реактивное приложение REST с использованием компонентов на основе аннотаций. Здесь мы будем использовать вместо этого функциональную структуру.
2. Зависимость от Maven
Нам понадобится та же зависимость spring-boot-starter-webflux
, что и в предыдущей статье:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.6.4</version>
</dependency>
3. Функциональная веб-инфраструктура
Функциональная веб-инфраструктура представляет новую модель программирования, в которой мы используем функции для маршрутизации и обработки запросов.
В отличие от модели на основе аннотаций, в которой мы используем сопоставления аннотаций, здесь мы будем использовать HandlerFunction
и RouterFunction
s .
Точно так же, как и в аннотированных контроллерах, подход функциональных конечных точек построен на том же реактивном стеке.
3.1. ОбработчикФункция
HandlerFunction представляет собой
функцию, которая генерирует ответы на направленные им запросы:
@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
Mono<T> handle(ServerRequest request);
}
Этот интерфейс в первую очередь представляет собой Function<Request, Response<T>>
, который ведет себя очень похоже на сервлет.
Хотя по сравнению со стандартным Servlet#service(ServletRequest req, ServletResponse res)
HandlerFunction не
принимает ответ в качестве входного параметра.
3.2. МаршрутизаторФункция
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. Реактивное приложение REST с использованием функциональной сети
В нашем предыдущем руководстве мы создали простое REST-приложение EmployeeManagement , используя
@RestController
и WebClient.
Теперь давайте реализуем ту же логику, используя функции маршрутизатора и обработчика.
Во- первых, нам нужно создать маршруты с помощью RouterFunction
для публикации и использования наших реактивных потоков Employee
s .
Маршруты регистрируются как компоненты Spring и могут быть созданы внутри любого класса конфигурации.
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 .
Второй параметр определяет функцию-обработчик, которая будет использоваться в случае применения предиката.
Другими словами, приведенный выше пример направляет все запросы GET для /employees/{id}
в метод 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()
для объединения наших маршрутов.
Наконец, мы реализовали полный REST API, необходимый для нашего приложения EmployeeManagement
, используя маршрутизаторы и обработчики.
Для запуска приложения мы можем использовать либо отдельные маршруты, либо единый составной маршрут, который мы создали выше.
6. Тестирование маршрутов
Мы можем использовать WebTestClient
для проверки наших маршрутов.
Для этого нам сначала нужно связать маршруты с помощью метода bindToRouterFunction
, а затем создать экземпляр тестового клиента.
Давайте проверим наш getEmployeeByIdRoute
:
@Test
public 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
public 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
, утверждая, что наш экземпляр Employee
обновляется через EmployeeRepository
:
@Test
public 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 и рассмотрели два ее основных интерфейса — RouterFunction
и HandlerFunction.
Мы также научились создавать различные маршруты для обработки запроса и отправки ответа.
Кроме того, мы воссоздали наше приложение EmployeeManagement
, представленное в руководстве по Spring 5 WebFlux, с функциональной моделью конечных точек.
Как всегда, полный исходный код можно найти на Github .