JavaRush /Курсы /Модуль 5. Spring /Spring Boot и WebFlux

Spring Boot и WebFlux

Модуль 5. Spring
16 уровень , 4 лекция
Открыта

Spring Boot упрощает разработку реактивных веб-приложений, предоставляя автоконфигурацию для Spring Webflux.

Фреймворк "Spring WebFlux"

Spring WebFlux – это новый реактивный веб-фреймворк, представленный в Spring Framework 5.0. В отличие от Spring MVC, он не требует наличия API сервлетов, является полностью асинхронным и неблокирующим, а также реализует спецификацию Reactive Streams через проект Reactor.

Spring WebFlux поставляется в двух вариантах: на основе функциональной модели и на основе аннотаций. Модель, основанная на аннотациях, довольно близка к модели Spring MVC, как показано в следующем примере:

Java
import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/users") public class MyRestController { private final UserRepository userRepository; private final CustomerRepository customerRepository; public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) { this.userRepository = userRepository; this.customerRepository = customerRepository; } @GetMapping("/{userId}") public Mono<User> getUser(@PathVariable Long userId) { return this.userRepository.findById(userId); } @GetMapping("/{userId}/customers") public Flux<Customer> getUserCustomers(@PathVariable Long userId) { return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser); } @DeleteMapping("/{userId}") public Mono<Void> deleteUser(@PathVariable Long userId) { return this.userRepository.deleteById(userId); } } 
Kotlin
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController @RequestMapping("/users") class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) { @GetMapping("/{userId}") fun getUser(@PathVariable userId: Long): Mono<User?> { return userRepository.findById(userId) } @GetMapping("/{userId}/customers") fun getUserCustomers(@PathVariable userId: Long): Flux<Customer> { return userRepository.findById(userId).flatMapMany { user: User? -> customerRepository.findByUser(user) } } @DeleteMapping("/{userId}") fun deleteUser(@PathVariable userId: Long): Mono<Void> { return userRepository.deleteById(userId) } } 

"WebFlux.fn", вариант, основанный на функциональной модели, отделяет конфигурацию маршрутизации от фактической обработки запросов, как показано в следующем примере:

Java
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RouterFunctions.route; @Configuration(proxyBeanMethods = false) public class MyRoutingConfiguration { private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON); @Bean public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) { return route() .GET("/{user}", ACCEPT_JSON, userHandler::getUser) .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers) .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser) .build(); } } 
Kotlin
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.MediaType import org.springframework.web.reactive.function.server.RequestPredicates.DELETE import org.springframework.web.reactive.function.server.RequestPredicates.GET import org.springframework.web.reactive.function.server.RequestPredicates.accept import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.RouterFunctions import org.springframework.web.reactive.function.server.ServerResponse @Configuration(proxyBeanMethods = false) class MyRoutingConfiguration { @Bean fun monoRouterFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> { return RouterFunctions.route( GET("/{user}").and(ACCEPT_JSON), userHandler::getUser).andRoute( GET("/{user}/customers").and(ACCEPT_JSON), userHandler::getUserCustomers).andRoute( DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser) } companion object { private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON) } } 
Java
import reactor.core.publisher.Mono; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; @Component public class MyUserHandler { public Mono<ServerResponse> getUser(ServerRequest request) { ... } public Mono<ServerResponse> getUserCustomers(ServerRequest request) { ... } public Mono<ServerResponse> deleteUser(ServerRequest request) { ... } } 
Kotlin
import org.springframework.stereotype.Component import org.springframework.web.reactive.function.server.ServerRequest import org.springframework.web.reactive.function.server.ServerResponse import reactor.core.publisher.Mono @Component class MyUserHandler { fun getUser(request: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } fun deleteUser(request: ServerRequest?): Mono<ServerResponse> { return ServerResponse.ok().build() } } 
Чтобы модулировать определение маршрутизатора, можно определять столько бинов RouterFunction, сколько пожелаете. Бины можно упорядочить, если требуется применять очередность выполнения.

Чтобы начать работу, добавьте модуль spring-boot-starter-webflux в свое приложение.

Добавление в приложение модулей spring-boot-starter-web и spring-boot-starter-webflux приведет к тому, что Spring Boot автоматически сконфигурирует Spring MVC, а не WebFlux. Такая логика работы была выбрана потому, что многие разработчики Spring добавляют spring-boot-starter-webflux в свои Spring MVC приложения для использования реактивного WebClient. Выбирать все еще можно самостоятельно, установив выбранный тип приложения в SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE).

Автоконфигурация Spring WebFlux

Spring Boot предусматривает автоконфигурацию для Spring WebFlux, которая отлично работает с большинством приложений.

Автоконфигурация привносит следующие функции вдобавок к настройкам Spring по умолчанию:

  • Конфигурирование кодеков для экземпляров HttpMessageReader и HttpMessageWriter.

  • Средства поддержки для обработки статических ресурсов, включая средства поддержки для WebJar.

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

Если необходимо полностью контролировать Spring WebFlux, то можно добавить свою собственную @Configuration, аннотированную @EnableWebFlux.

HTTP-кодеки через HttpMessageReaders и HttpMessageWriters

Spring WebFlux использует интерфейсы HttpMessageReader и HttpMessageWriter для преобразования запросов и ответов HTTP. Они конфигурируются при помощи CodecConfigurer с адекватными значениями путем просмотра библиотек, доступных в вашем classpath.

Spring Boot предусматривает специализированные конфигурационные свойства для кодеков, spring.codec.*. Фреймворк также применяет дальнейшую настройку с помощью экземпляров CodecCustomizer. Например, конфигурационные ключи spring.jackson.* применяются к кодеку библиотеки Jackson.

Если необходимо добавить или настроить кодеки, то можно создать кастомный компонент CodecCustomizer, как показано в следующем примере:

Java
import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.codec.ServerSentEventHttpMessageReader; @Configuration(proxyBeanMethods = false) public class MyCodecsConfiguration { @Bean public CodecCustomizer myCodecCustomizer() { return (configurer) -> { configurer.registerDefaults(false); configurer.customCodecs().register(new ServerSentEventHttpMessageReader()); // ... }; } } 
Kotlin
import org.springframework.boot.web.codec.CodecCustomizer import org.springframework.context.annotation.Bean import org.springframework.http.codec.CodecConfigurer import org.springframework.http.codec.ServerSentEventHttpMessageReader class MyCodecsConfiguration { @Bean fun myCodecCustomizer(): CodecCustomizer { return CodecCustomizer { configurer: CodecConfigurer -> configurer.registerDefaults(false) configurer.customCodecs().register(ServerSentEventHttpMessageReader()) } } } 

Кроме того, можно использовать кастомные сериализаторы и десериализаторы JSON в Spring Boot.

Статическое содержимое

По умолчанию Spring Boot обрабатывает статическое содержимое из каталога /static (или /public, или /resources, или /META-INF/resources) в classpath. Фреймворк использует ResourceWebHandler из Spring WebFlux, поэтому можно изменить такую логику работы, добавив свой собственный WebFluxConfigurer и переопределив метод addResourceHandlers.

По умолчанию ресурсы отображаются на /**, но можно тонко настроить это отображение, установив свойство spring.webflux.static-path-pattern. Например, перемещение всех ресурсов в /resources/** можно выполнить следующим образом:

Properties
spring.webflux.static-path-pattern=/resources/**
Yaml
spring:
  webflux:
    static-path-pattern: "/resources/**"

Помимо этого, можно настраивать местоположения статических ресурсов с помощью spring.web.resources.static-locations. При этом значения по умолчанию заменяются списком местоположений каталогов. При таких настройках стандартное средство обнаружения начальной страницы переключится на ваши кастомные местоположения. Таким образом, если при запуске в любом из ваших местоположений находится index.html, то она станет домашней страницей приложения.

В дополнение к "стандартным" метосположениям статических ресурсов, перечисленным ранее, особый сценарий предусмотрен для содержимого Webjars. Любые ресурсы с путем в /webjars/** обрабатываются из jar-файлов, если они упакованы в формат Webjars.

Приложения Spring WebFlux не зависят от API сервлетов, поэтому они их можно развернуть как war-файлы, и они не будут использовать каталог src/main/webapp.

Начальная страница

Spring Boot поддерживает как статические, так и шаблонные начальные страницы. Сначала фреймворк ищет файл index.html в сконфигурированных местоположениях статического содержимого. Если такой шаблон не найден, он ищет шаблон index. Если один из них будет найден, то он будет автоматически использован в качестве начальной страницы приложения.

Шаблонизаторы

Помимо веб-служб REST, для обработки динамического HTML-содержимого можно также использовать Spring WebFlux. Spring WebFlux поддерживает различные технологии шаблонизации, включая Thymeleaf, FreeMarker и Mustache.

Spring Boot предусматривает поддержку автоконфигурации для следующих шаблонизаторов:

Если один из этих шаблонизаторов используется с конфигурацией по умолчанию, то шаблоны автоматически подбираются из src/main/resources/templates.

Обработка ошибок

Spring Boot предусматривает WebExceptionHandler, который обрабатывает все ошибки адекватным образом. Его позиция в порядке обработки находится непосредственно перед обработчиками, предоставляемыми WebFlux, которые учитываются последними. Для машинных клиентов он создает ответ в формате JSON с подробным описанием ошибки, кодом состояния HTTP и сообщением об исключении. Для браузерных клиентов существует "whitelabel" обработчик ошибок, который визуализирует те же данные в формате HTML. Кроме того, можно предусмотреть собственные HTML-шаблоны для вывода на экран ошибок.

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

Чтобы изменить логику обработки ошибок, можно реализовать ErrorWebExceptionHandler и зарегистрировать определение бина этого типа. Поскольку ErrorWebExceptionHandler является достаточно низкоуровневым, Spring Boot также предусматривает вспомогательный AbstractErrorWebExceptionHandler, позволяющий обрабатывать ошибки функциональным способом через WebFlux, как показано в следующем примере:

Java
import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder; @Component public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler { public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, ApplicationContext applicationContext) { super(errorAttributes, resources, applicationContext); } @Override protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) { return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml); } private boolean acceptsXml(ServerRequest request) { return request.headers().accept().contains(MediaType.APPLICATION_XML); } public Mono<ServerResponse> handleErrorAsXml(ServerRequest request) { BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR); // ... дополнительные вызовы сресдтва сборки return builder.build(); } } 
Kotlin
import org.springframework.boot.autoconfigure.web.WebProperties import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler import org.springframework.boot.web.reactive.error.ErrorAttributes import org.springframework.context.ApplicationContext import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.stereotype.Component import org.springframework.web.reactive.function.server.RouterFunction import org.springframework.web.reactive.function.server.RouterFunctions import org.springframework.web.reactive.function.server.ServerRequest import org.springframework.web.reactive.function.server.ServerResponse import reactor.core.publisher.Mono @Component class MyErrorWebExceptionHandler(errorAttributes: ErrorAttributes?, resources: WebProperties.Resources?, applicationContext: ApplicationContext?) : AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) { override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> { return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml) } private fun acceptsXml(request: ServerRequest): Boolean { return request.headers().accept().contains(MediaType.APPLICATION_XML) } fun handleErrorAsXml(request: ServerRequest?): Mono<ServerResponse> { val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) // ... дополнительные вызовы сресдтва сборки return builder.build() } } 

Для более полной картины можно также разбить DefaultErrorWebExceptionHandler на подклассы напрямую и переопределить конкретные методы.

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

Java
import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.reactive.result.view.Rendering; import org.springframework.web.server.ServerWebExchange; @Controller public class MyExceptionHandlingController { @GetMapping("/profile") public Rendering userProfile() { // ... throw new IllegalStateException(); } @ExceptionHandler(IllegalStateException.class) public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) { exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc); return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build(); } } 
Kotlin
import org.springframework.boot.web.reactive.error.ErrorAttributes import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.reactive.result.view.Rendering import org.springframework.web.server.ServerWebExchange @Controller class MyExceptionHandlingController { @GetMapping("/profile") fun userProfile(): Rendering { // ... throw IllegalStateException() } @ExceptionHandler(IllegalStateException::class) fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering { exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc) return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build() } } 

Кастомные страницы ошибок

Если необходимо вывести на экран кастомную HTML-страницу ошибки для заданного кода состояния, можно добавить файл в каталог /error. Страницы ошибок могут быть либо статическими HTML (то есть добавляемыми в любой из каталогов статических ресурсов), либо созданными с помощью шаблонов. Имя файла должно быть точным кодом состояния или маской серии.

Например, чтобы отобразить 404 со статическим HTML-файлом, структура каталогов должна выглядеть следующим образом:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

Чтобы отобразить все ошибки 5xx с помощью шаблона Mustache, структура каталога должна выглядеть так:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.mustache
             +- <other templates>

Веб-фильтры

Spring WebFlux предусматривает интерфейс WebFilter, который может быть реализован для фильтрации обмена запросами и ответами HTTP. Бины WebFilter, найденные в контексте приложения, будут автоматически использоваться для фильтрации каждого случая обмена.

Если порядок фильтров имеет значение, они могут реализовать класс Ordered или же их можно пометить аннотацией @Order. Автоконфигурация Spring Boot может сконфигурировать веб-фильтры за вас. В таком случае будет использоваться способы упорядочивания, показанные в следующей таблице:

Веб-фильтр Порядок

MetricsWebFilter

Ordered.HIGHEST_PRECEDENCE + 1

WebFilterChainProxy (Spring Security)

-100

HttpTraceWebFilter

Ordered.LOWEST_PRECEDENCE - 10

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ