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