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 может сконфигурировать веб-фильтры за вас. В таком случае будет использоваться способы упорядочивания, показанные в следующей таблице:
Веб-фильтр | Порядок |
---|---|
|
|
|
|
|
|
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ