[Java] Реактивное программирование со Spring, часть 3 WebFlux (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
30-Июн-2021 15:32

Это третья часть серии блогов о реактивном программировании, в которой я познакомлю вас с 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 И RSOCKET5.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: WebSockets5.2 RSOCKETRSocket - это протокол, моделирующий семантику реактивных потоков по сети. Это двоичный протокол для использования в транспортных потоках байтовых потоков, таких как TCP, WebSockets и Aeron. В качестве введения в эту тему я рекомендую следующий пост в блоге, который написал мой коллега Pär: An introduction to RSocketА для получения дополнительной информации о поддержке Spring Framework протокола RSocket6. ПОДВОДЯ ИТОГ…Это сообщение в блоге продемонстрировало, как WebFlux можно использовать для создания реактивного веб-приложения. В следующем и последнем посте этой серии будет показано, как мы можем сделать весь наш стек приложений полностью неблокирующим, также реализовав неблокирующую связь с базой данных - с помощью R2DBC (Reactive Relational Database Connectivity)!ССЫЛКИSpring Framework documentation - Web on Reactive StackSpring Boot Features - The Spring WebFlux frameworkSpring Security - Reactive Applications
===========
Источник:
habr.com
===========

===========
Автор оригинала: ANNA ERIKSSON
===========
Похожие новости: Теги для поиска: #_java, #_spring_boot_2, #_reactive_programming, #_webflux, #_java
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 12:05
Часовой пояс: UTC + 5