Это третья часть серии блогов о реактивном программировании, в которой я познакомлю вас с 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
ссылка на оригинал статьи https://habr.com/ru/post/565056/
Добавить комментарий