Spring Boot упрощает разработку реактивных веб-приложений, предоставляя автоконфигурацию для Spring Webflux.
Фреймворк "Spring WebFlux"
Spring WebFlux – это новый реактивный веб-фреймворк, представленный в Spring Framework 5.0. В отличие от Spring MVC, он не требует наличия API сервлетов, является полностью асинхронным и неблокирующим, а также реализует спецификацию Reactive Streams через проект Reactor.
Spring WebFlux поставляется в двух вариантах: на основе функциональной модели и на основе аннотаций. Модель, основанная на аннотациях, довольно близка к модели Spring MVC, как показано в следующем примере:
@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); } }
@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", вариант, основанный на функциональной модели, отделяет конфигурацию маршрутизации от фактической обработки запросов, как показано в следующем примере:
@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(); } }
@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) } }
@Component public class MyUserHandler { public Mono<ServerResponse> getUser(ServerRequest request) { ... } public Mono<ServerResponse> getUserCustomers(ServerRequest request) { ... } public Mono<ServerResponse> deleteUser(ServerRequest request) { ... } }
@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, как показано в следующем примере:
@Configuration(proxyBeanMethods = false) public class MyCodecsConfiguration { @Bean public CodecCustomizer myCodecCustomizer() { return (configurer) -> { configurer.registerDefaults(false); configurer.customCodecs().register(new 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/** можно выполнить следующим образом:
spring.webflux.static-path-pattern=/resources/**
spring:
webflux:
static-path-pattern: "/resources/**"
Помимо этого, можно настраивать местоположения статических ресурсов с помощью spring.web.resources.static-locations. При этом значения по умолчанию заменяются списком местоположений каталогов. При таких настройках стандартное средство обнаружения начальной страницы переключится на ваши кастомные местоположения. Таким образом, если при запуске в любом из ваших местоположений находится index.html, то она станет домашней страницей приложения.
В дополнение к "стандартным" метосположениям статических ресурсов, перечисленным ранее, особый сценарий предусмотрен для содержимого Webjars. Любые ресурсы с путем в /webjars/** обрабатываются из jar-файлов, если они упакованы в формат Webjars.
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, как показано в следующем примере:
@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(); } }
@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 на подклассы напрямую и переопределить конкретные методы.
В некоторых случаях ошибки, обрабатываемые на уровне контроллера или функции-обработчика, не регистрируются инфраструктурой метрик. Приложения могут обеспечить запись таких исключений в метрики запроса, настроив обработанное исключение в качестве атрибута запроса:
@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(); } }
@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 может сконфигурировать веб-фильтры за вас. В таком случае будет использоваться способы упорядочивания, показанные в следующей таблице:
| Веб-фильтр | Порядок |
|---|---|
|
|
|
|
|
|
|
|
|
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ