Реактивное программирование со Spring, часть 3 WebFlux

Это третья часть серии блогов о реактивном программировании, в которой я познакомлю вас с WebFlux — реактивным веб-фреймворком Spring.

1. ВВЕДЕНИЕ В SPRING WEBFLUX

Исходный веб-фреймворк для Spring — Spring Web MVC — был построен для Servlet API и контейнеров Servlet.

WebFlux был представлен как часть Spring Framework 5.0. В отличие от Spring MVC, он не требует Servlet API. Он полностью асинхронный и неблокирующий, реализует спецификацию Reactive Streams через проект Reactor (см. предыдущий пост в блоге ).

WebFlux требует Reactor в качестве основной зависимости, но он также может взаимодействовать с другими реактивными библиотеками через Reactive Streams.

1.1 МОДЕЛИ ПРОГРАММИРОВАНИЯ

Spring WebFlux поддерживает две разные модели программирования: на основе аннотаций и функциональную.

1.1.1 АННОТИРОВАННЫЕ КОНТРОЛЛЕРЫ

Если вы работали со Spring MVC, модель на основе аннотаций будет выглядеть довольно знакомой, поскольку в ней используются те же аннотации из веб-модуля Spring, что и в Spring MVC. Основное отличие состоит в том, что теперь методы возвращают реактивные типы Mono и Flux. См. Следующий пример RestController с использованием модели на основе аннотаций:

@RestController @RequestMapping("/students") public class StudentController {      @Autowired     private StudentService studentService;       public StudentController() {     }      @GetMapping("/{id}")     public Mono<ResponseEntity<Student>> getStudent(@PathVariable long id) {         return studentService.findStudentById(id)                 .map(ResponseEntity::ok)                 .defaultIfEmpty(ResponseEntity.notFound().build());     }      @GetMapping     public Flux<Student> listStudents(@RequestParam(name = "name", required = false) String name) {         return studentService.findStudentsByName(name);     }      @PostMapping     public Mono<Student> addNewStudent(@RequestBody Student student) {         return studentService.addNewStudent(student);     }      @PutMapping("/{id}")     public Mono<ResponseEntity<Student>> updateStudent(@PathVariable long id, @RequestBody Student student) {         return studentService.updateStudent(id, student)                 .map(ResponseEntity::ok)                 .defaultIfEmpty(ResponseEntity.notFound().build());     }      @DeleteMapping("/{id}")     public Mono<ResponseEntity<Void>> deleteStudent(@PathVariable long id) {         return studentService.findStudentById(id)                 .flatMap(s ->                         studentService.deleteStudent(s)                                 .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))                 )                 .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));     } }

Некоторые пояснения к функциям, использованным в примере:

  • map функция используется для преобразования элемента, испускаемого Mono, применяя функцию синхронной к нему.

  • flatMap функция используется для преобразования элемент, испускаемый Mono асинхронно, возвращая значение, излучаемого другим Mono.

  • defaultIfEmpty функция обеспечивает значение по умолчанию, если Mono завершается без каких — либо данных.

1.1.2 ФУНКЦИОНАЛЬНЫЕ КОНЕЧНЫЕ ТОЧКИ

Модель функционального программирования основана на лямбда-выражении и оставляет за приложением полную обработку запроса. Он основан на концепциях HandlerFunctions и RouterFunctions.

HandlerFunctions используются для генерации ответа на данный запрос:

@FunctionalInterface public interface HandlerFunction<T extends ServerResponse> {     Mono<T> handle(ServerRequest request); } 

RouterFunction используется для маршрутизации запросов к HandlerFunctions:

@FunctionalInterface public interface RouterFunction<T extends ServerResponse> {     Mono<HandlerFunction<T>> route(ServerRequest request);     ... } 

Продолжая с тем же примером ученика, мы получим что-то вроде следующего, используя функциональный стиль.

A StudentRouter:

@Configuration public class StudentRouter {      @Bean     public RouterFunction<ServerResponse> route(StudentHandler studentHandler){         return RouterFunctions             .route(                 GET("/students/{id:[0-9]+}")                     .and(accept(APPLICATION_JSON)), studentHandler::getStudent)             .andRoute(                 GET("/students")                     .and(accept(APPLICATION_JSON)), studentHandler::listStudents)             .andRoute(                 POST("/students")                     .and(accept(APPLICATION_JSON)),studentHandler::addNewStudent)             .andRoute(                 PUT("students/{id:[0-9]+}")                     .and(accept(APPLICATION_JSON)), studentHandler::updateStudent)             .andRoute(                 DELETE("/students/{id:[0-9]+}")                     .and(accept(APPLICATION_JSON)), studentHandler::deleteStudent);     } }

И StudentHandler:

@Component public class StudentHandler {      private StudentService studentService;      public StudentHandler(StudentService studentService) {         this.studentService = studentService;     }      public Mono<ServerResponse> getStudent(ServerRequest serverRequest) {         Mono<Student> studentMono = studentService.findStudentById(                 Long.parseLong(serverRequest.pathVariable("id")));         return studentMono.flatMap(student -> ServerResponse.ok()                 .body(fromValue(student)))                 .switchIfEmpty(ServerResponse.notFound().build());     }      public Mono<ServerResponse> listStudents(ServerRequest serverRequest) {         String name = serverRequest.queryParam("name").orElse(null);         return ServerResponse.ok()                 .contentType(MediaType.APPLICATION_JSON)                 .body(studentService.findStudentsByName(name), Student.class);     }      public Mono<ServerResponse> addNewStudent(ServerRequest serverRequest) {         Mono<Student> studentMono = serverRequest.bodyToMono(Student.class);         return studentMono.flatMap(student ->                 ServerResponse.status(HttpStatus.OK)                         .contentType(MediaType.APPLICATION_JSON)                         .body(studentService.addNewStudent(student), Student.class));      }      public Mono<ServerResponse> updateStudent(ServerRequest serverRequest) {         final long studentId = Long.parseLong(serverRequest.pathVariable("id"));         Mono<Student> studentMono = serverRequest.bodyToMono(Student.class);          return studentMono.flatMap(student ->                 ServerResponse.status(HttpStatus.OK)                         .contentType(MediaType.APPLICATION_JSON)                         .body(studentService.updateStudent(studentId, student), Student.class));     }      public Mono<ServerResponse> deleteStudent(ServerRequest serverRequest) {         final long studentId = Long.parseLong(serverRequest.pathVariable("id"));         return studentService                 .findStudentById(studentId)                 .flatMap(s -> ServerResponse.noContent().build(studentService.deleteStudent(s)))                 .switchIfEmpty(ServerResponse.notFound().build());     } }

Некоторые пояснения к функциям, использованным в примере:

  • switchIfEmpty функция имеет ту же цель, defaultIfEmpty, но вместо того, чтобы обеспечить значение по умолчанию, она используется для обеспечения альтернативного Mono.

Сравнивая две модели, мы видим, что:

  • Для использования функционального варианта требуется еще немного кода для таких вещей, как получение входных параметров и синтаксический анализ до ожидаемого типа.

  • Не полагаясь на аннотации, но написание явного кода предлагает некоторую большую гибкость и может быть лучшим выбором, если нам, например, нужно реализовать более сложную маршрутизацию.

1.2 ПОДДЕРЖКА СЕРВЕРА

WebFlux работает в средах выполнения, отличных от сервлетов, таких как Netty и Undertow (неблокирующий режим), а также в средах выполнения сервлетов 3.1+, таких как Tomcat и Jetty.

По умолчанию стартер Spring Boot WebFlux использует Netty, но его легко переключить, изменив зависимости Maven или Gradle.

Например, чтобы переключиться на Tomcat, просто исключите spring-boot-starter-netty из зависимости spring-boot-starter-webflux и добавьте spring-boot-starter-tomcat:

<dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-webflux</artifactId>     <exclusions>         <exclusion>             <groupId>org.springframework.boot</groupId>             <artifactId>spring-boot-starter-netty</artifactId>         </exclusion>     </exclusions> </dependency> <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-tomcat</artifactId> </dependency>

1.3 КОНФИГУРАЦИЯ

Spring Boot обеспечивает автоматическую настройку Spring WebFlux, которая хорошо работает в общих случаях. Если вам нужен полный контроль над конфигурацией WebFlux, можно использовать аннотацию @EnableWebFlux (эта аннотация также потребуется в простом приложении Spring для импорта конфигурации Spring WebFlux).

Если вы хотите сохранить конфигурацию Spring Boot WebFlux и просто добавить дополнительную конфигурацию WebFlux, вы можете добавить свой собственный класс @Configuration типа WebFluxConfigurer (но без @EnableWebFlux).

Подробные сведения и примеры см. в документации по конфигурации WebFlux.

2. ЗАЩИТА ВАШИХ КОНЕЧНЫХ ТОЧЕК

Чтобы получить поддержку Spring Security WebFlux, сначала добавьте в свой проект зависимость spring-boot-starter-security. Теперь вы можете включить его, добавив @EnableWebFluxSecurity аннотацию в свой класс Configuration (доступно с Spring Security 5.0).

В следующем упрощенном примере будет добавлена ​​поддержка двух пользователей, один с ролью USER, а другой с ролью ADMIN, принудительно применить базовую аутентификацию HTTP и потребовать роль ADMIN для любого доступа к пути /student/admin:

@EnableWebFluxSecurity public class SecurityConfig {      @Bean     public MapReactiveUserDetailsService userDetailsService() {          UserDetails user = User                 .withUsername("user")                 .password(passwordEncoder().encode("userpwd"))                 .roles("USER")                 .build();          UserDetails admin = User                 .withUsername("admin")                 .password(passwordEncoder().encode("adminpwd"))                 .roles("ADMIN")                 .build();          return new MapReactiveUserDetailsService(user, admin);     }      @Bean     public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {         return http.authorizeExchange()                 .pathMatchers("/students/admin")                 .hasAuthority("ROLE_ADMIN")                 .anyExchange()                 .authenticated()                 .and().httpBasic()                 .and().build();     }      @Bean     public PasswordEncoder passwordEncoder() {         return new BCryptPasswordEncoder();     }  }

Также можно защитить метод, а не путь, сначала добавив аннотацию @EnableReactiveMethodSecurity к вашей конфигурации:

@EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SecurityConfig {     ... }

А затем добавляем @PreAuthorize аннотацию к защищаемым методам. Например, мы можем захотеть, чтобы наши методы POST, PUT и DELETE были доступны только для роли ADMIN. Затем к этим методам можно применить аннотацию PreAuthorize, например:

@DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") public Mono<ResponseEntity<Void>> deleteStudent(@PathVariable long id) {     ... } 

Spring Security предлагает дополнительную поддержку, связанную с приложениями WebFlux, например защиту CSRF, интеграцию OAuth2 и реактивную аутентификацию X.509. Для получения дополнительной информации прочтите следующий раздел в документации Spring Security: Реактивные приложения

3. ВЕБ-КЛИЕНТ

Spring WebFlux также включает реактивный, полностью неблокирующий веб-клиент. У него есть функциональный, свободный API, основанный на Reactor.

Давайте рассмотрим (еще раз) упрощенный пример того, как WebClient можно использовать для запроса нашего StudentController:

public class StudentWebClient {      WebClient client = WebClient.create("http://localhost:8080");          public Mono<Student> get(long id) {             return client                     .get()                     .uri("/students/" + id)                     .headers(headers -> headers.setBasicAuth("user", "userpwd"))                     .retrieve()                     .bodyToMono(Student.class);         }              public Flux<Student> getAll() {             return client.get()                     .uri("/students")                     .headers(headers -> headers.setBasicAuth("user", "userpwd"))                     .retrieve()                     .bodyToFlux(Student.class);         }              public Flux<Student> findByName(String name) {             return client.get()                     .uri(uriBuilder -> uriBuilder.path("/students")                     .queryParam("name", name)                     .build())                     .headers(headers -> headers.setBasicAuth("user", "userpwd"))                     .retrieve()                     .bodyToFlux(Student.class);         }              public Mono<Student> create(Student s)  {             return client.post()                     .uri("/students")                     .headers(headers -> headers.setBasicAuth("admin", "adminpwd"))                     .body(Mono.just(s), Student.class)                     .retrieve()                     .bodyToMono(Student.class);         }              public Mono<Student> update(Student student)  {             return client                     .put()                     .uri("/students/" + student.getId())                     .headers(headers -> headers.setBasicAuth("admin", "adminpwd"))                     .body(Mono.just(student), Student.class)                     .retrieve()                     .bodyToMono(Student.class);         }              public Mono<Void> delete(long id) {             return client                     .delete()                     .uri("/students/" + id)                     .headers(headers -> headers.setBasicAuth("admin", "adminpwd"))                     .retrieve()                     .bodyToMono(Void.class);         } }

4. ТЕСТИРОВАНИЕ

Для тестирования вашего реактивного веб-приложения WebFlux предлагает WebTestClient, который поставляется с API, аналогичным WebClient.

Давайте посмотрим, как мы можем протестировать наш StudentController с помощью WebTestClient:

@ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class StudentControllerTest {     @Autowired     WebTestClient webClient;      @Test     @WithMockUser(roles = "USER")     void test_getStudents() {         webClient.get().uri("/students")                 .header(HttpHeaders.ACCEPT, "application/json")                 .exchange()                 .expectStatus().isOk()                 .expectHeader().contentType(MediaType.APPLICATION_JSON)                 .expectBodyList(Student.class);      }      @Test     @WithMockUser(roles = "ADMIN")     void testAddNewStudent() {         Student newStudent = new Student();         newStudent.setName("some name");         newStudent.setAddress("an address");          webClient.post().uri("/students")                 .contentType(MediaType.APPLICATION_JSON)                 .accept(MediaType.APPLICATION_JSON)                 .body(Mono.just(newStudent), Student.class)                 .exchange()                 .expectStatus().isOk()                 .expectHeader().contentType(MediaType.APPLICATION_JSON)                 .expectBody()                 .jsonPath("$.id").isNotEmpty()                 .jsonPath("$.name").isEqualTo(newStudent.getName())                 .jsonPath("$.address").isEqualTo(newStudent.getAddress());     }      ... } 

5. WEBSOCKETS И RSOCKET

5.1 ВЕБ-СОКЕТЫ

В Spring 5 WebSockets также получает дополнительные реактивные возможности. Чтобы создать сервер WebSocket, вы можете создать реализацию WebSocketHandler интерфейса, которая содержит следующий метод:

Mono<Void> handle(WebSocketSession session)

Этот метод вызывается при установке нового соединения WebSocket и позволяет обрабатывать сеанс. Он принимает в WebSocketSession качестве входных данных и возвращает Mono <Void>, чтобы сигнализировать о завершении обработки сеанса приложением.

WebSocketSession имеет методы, определенные для обработки входящих и исходящих потоков:

Flux<WebSocketMessage> receive() Mono<Void> send(Publisher<WebSocketMessage> messages)

Spring WebFlux также предоставляет WebSocketClient реализации для Reactor Netty, Tomcat, Jetty, Undertow и стандартной Java.

Для получения дополнительной информации прочтите следующую главу в документации Spring’s Web on Reactive Stack: WebSockets

5.2 RSOCKET

RSocket — это протокол, моделирующий семантику реактивных потоков по сети. Это двоичный протокол для использования в транспортных потоках байтовых потоков, таких как TCP, WebSockets и Aeron. В качестве введения в эту тему я рекомендую следующий пост в блоге, который написал мой коллега Pär: An introduction to RSocket

А для получения дополнительной информации о поддержке Spring Framework протокола RSocket

6. ПОДВОДЯ ИТОГ…

Это сообщение в блоге продемонстрировало, как WebFlux можно использовать для создания реактивного веб-приложения. В следующем и последнем посте этой серии будет показано, как мы можем сделать весь наш стек приложений полностью неблокирующим, также реализовав неблокирующую связь с базой данных — с помощью R2DBC (Reactive Relational Database Connectivity)!

ССЫЛКИ

Spring Framework documentation — Web on Reactive Stack

Spring Boot Features — The Spring WebFlux framework

Spring Security — Reactive Applications

ссылка на оригинал статьи https://habr.com/ru/post/565056/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *