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

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

Spring WebFlux — це новий реактивний вебфреймворк, представлений у Spring Framework 5.0. На відміну від Spring MVC, він не вимагає наявності API сервлетів, є повністю асинхронним та неблокуючим, а також реалізує специфікацію Reactive Streams через проєкт Reactor.

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

block-title">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();
        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()
        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