Spring Boot спрощує розробку реактивних вебдодатків, надаючи автоконфігурацію для Spring Webflux.
Фреймворк "Spring WebFlux"
Spring WebFlux — це новий реактивний вебфреймворк, представлений у Spring Framework 5.0. На відміну від Spring MVC, він не вимагає наявності API сервлетів, є повністю асинхронним та неблокуючим, а також реалізує специфікацію Reactive Streams через проєкт Reactor.
Spring WebFlux поставляється у двох варіантах: на основі функціональної моделі та на основі анотацій. Модель, заснована на анотаціях, досить близька до моделі Spring MVC, як показано в наступному прикладі:
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);
}
}
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", варіант, заснований на функціональній моделі, відокремлює конфігурацію маршрутизації від фактичної обробки запитів, як показано в наведеному нижче прикладі:
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();
}
}
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)
}
}
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) {
...
}
}
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
, як показано в наступному прикладі:
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());
// ...
};
}
}
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/**
можна виконати так:
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, як показано в наступному прикладі:
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();
}
}
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
на підкласи безпосередньо та перевизначити конкретні методи.
У деяких випадках помилки, що обробляються на рівні контролера або функції-обробника, не реєструються інфраструктурою метрик. Програми можуть забезпечити запис таких винятків до метрик запиту, налаштувавши оброблений виняток як атрибут запиту:
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();
}
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 може конфігурувати вебфільтри за тебе. У такому випадку будуть використовуватися
способи упорядкування, показані в таблиці:
Веб-фільтр | Порядок |
---|---|
|
|
|
|
|
|