• Для обработки запросов сервера существует два уровня средств поддержки.

    • HttpHandler: Базовый контракт для обработки HTTP-запросов с неблокирующим вводом-выводом и обратной реакцией по спецификации Reactive Streams, а также адаптеры для Reactor Netty, Undertow, Tomcat, Jetty и любого контейнера Servlet 3.1+.

    • WebHandler API: Немного более высокоуровневый веб-API общего назначения для обработки запросов, на основе которого строятся конкретные модели программирования, такие как аннотированные контроллеры и функциональные конечные точки.

  • Для клиентской части существует базовый контракт ClientHttpConnector для выполнения HTTP-запросов с неблокирующим вводом-выводом и обратной реакцией по спецификации Reactive Streams, а также адаптеры для Reactor Netty, реактивных Jetty HttpClient и Apache HttpComponents. Более высокоуровневый WebClient, используемый в приложениях, основывается на этом базовом контракте.

  • Для клиента и сервера предусмотрены кодеки для сериализации и десериализации содержимого запросов и ответов HTTP.

HttpHandler

HttpHandler — это простой контракт с единственным методом для обработки запроса и ответа. Он намеренно строг, а его главная и единственная цель — быть простой абстракцией над различными API HTTP-серверов.

В следующей таблице описаны поддерживаемые серверные API:

Имя сервера Используемый API сервера Поддержка Reactive Streams

Netty

Netty API

Reactor Netty

Undertow

Undertow API

spring-web: Подводка к мосту Reactive Streams

Tomcat

Неблокирующий ввод-вывод на базе Servlet 3.1; Tomcat API для чтения и записи ByteBuffers против byte[]

spring-web: Неблокирующий ввод-вывод Servlet 3.1 в мост Reactive Streams

Jetty

Неблокирующий ввод-вывод на базе Servlet 3.1; Jetty API для записи ByteBuffers против byte[]

spring-web: Неблокирующий ввод-вывод Servlet 3.1 в мост Reactive Streams

Servlet 3.1 container

Неблокирующий ввод/вывод на базе Servlet 3.1

spring-web: Неблокирующий ввод-вывод Servlet 3.1 в мост Reactive Streams

В следующей таблице описаны зависимости от сервера (также см. поддерживаемые версии):

Имя сервера Идентификатор группы Название артефакта

Reactor Netty

io.projectreactor.netty

reactor-netty

Undertow

io.undertow

undertow-core

Tomcat

org.apache.tomcat.embed

tomcat-embed-core

Jetty

org.eclipse.jetty

jetty-server, jetty-servlet

В приведенных ниже фрагментах кода показано использование адаптеров HttpHandler с каждым серверным API:

Reactor Netty

Java
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
Kotlin
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()

Undertow

Java
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
Kotlin
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()

Tomcat

Java
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
Kotlin
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()

Jetty

Java
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
Kotlin
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()

Контейнер Servlet 3.1+

Чтобы развернуть WAR-файл в любом контейнере Servlet 3.1+, можно расширить и включить AbstractReactiveWebInitializer в WAR-файл. Этот класс оборачивает HttpHandler в ServletHttpHandlerAdapter и регистрирует его как Servlet.

WebHandler API

Пакет org.springframework.web.server основан на контракте HttpHandler и предоставляет веб-API общего назначения для обработки запросов через цепочку из нескольких WebExceptionHandler, нескольких WebFilter и одного компонента WebHandler. Цепочка может быть собрана вместе с WebHttpHandlerBuilder путем простого указания на ApplicationContext из Spring, где компоненты определяются автоматически, и/или путем регистрации компонентов в конструкторе.

В то время как цель HttpHandler проста – абстрагировать использование различных HTTP-серверов, WebHandler API нацелен на предоставление более широкого набора функций, обычно используемых в веб-приложениях, таких как:

  • Пользовательская сессия с атрибутами.

  • Атрибуты запроса.

  • Разрешенная Locale или Principal для запроса.

  • Доступ к парсированным и кэшированным данным формы.

  • Абстракции для многокомпонентных данных.

  • и многое другое...

Специализированные виды бинов

В таблице ниже перечислены компоненты, которые WebHttpHandlerBuilder может автоматически обнаружить в ApplicationContext из Spring, или которые могут быть зарегистрированы непосредственно в нем:

Имя бина Тип бина Счетчик Описание

<any>

WebExceptionHandler

0..N

Обеспечивает обработку исключений из цепочки экземпляров WebFilter и целевого WebHandler.

<any>

WebFilter

0..N

Применяет логику в стиле перехвата перед и после остальной цепочки фильтров и целевого WebHandler.

webHandler

WebHandler

1

Обработчик запроса.

webSessionManager

WebSessionManager

0..1

Диспетчер для экземпляров WebSession, открываемых через метод для ServerWebExchange. DefaultWebSessionManager по умолчанию.

serverCodecConfigurer

ServerCodecConfigurer

0..1

Обеспечивает доступ к экземплярам HttpMessageReader для парсинга данных формы и многокомпонентных данных, которые затем открываются через методы для ServerWebExchange. ServerCodecConfigurer.create() by default.

localeContextResolver

LocaleContextResolver

0..1

Распознаватель для LocaleContext, открываемый через метод для ServerWebExchange. AcceptHeaderLocaleContextResolver по умолчанию.

forwardedHeaderTransformer

ForwardedHeaderTransformer

0..1

Предназначен для обработки заголовков пересылаемых типов либо путем их извлечения и удаления, либо только путем их удаления. По умолчанию не используется.

Данные формы

ServerWebExchange предоставляет следующий метод для доступа к данным формы:

Java
Mono<MultiValueMap<String, String>> getFormData();
Kotlin
suspend fun getFormData(): MultiValueMap<String, String>

The DefaultServerWebExchange uses the configured HttpMessageReader to parse form data (application/x-www-form-urlencoded) into a MultiValueMap. По умолчанию FormHttpMessageReader сконфигурирован под использование бином ServerCodecConfigurer.

Многокомпонентные данные

ServerWebExchange предоставляет следующий метод для доступа к многокомпонентным данным:

Java
Mono<MultiValueMap<String, Part>> getMultipartData();
Kotlin
suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchange использует сконфигурированный HttpMessageReader<MultiValueMap<String, Part>> для парсинга содержимого multipart/form-data в MultiValueMap. По умолчанию это DefaultPartHttpMessageReader, который не имеет никаких сторонних зависимостей. В качестве альтернативы можно использовать SynchronossPartHttpMessageReader, который основан на библиотеке Synchronoss NIO Multipart. Оба конфигурируются при помощи бина ServerCodecConfigurer.

Для потокового парсинга многокомпонентных данных можно использовать Flux<Part>, возвращаемый из HttpMessageReader<Part>. Например, в аннотированном контроллере использование @RequestPart подразумевает Map-подобный доступ к отдельным компонентам по имени и, следовательно, требует полного парсинга многокомпонентный данных. И наоборот, вы можете использовать аннотацию @RequestBody для декодирования содержимого в Flux<Part> без сбора в MultiValueMap.

Пересылаемые заголовки

Если запрос проходит через прокси-серверы (например, распределители нагрузки), хост, порт и схема могут меняться. Это усложняет задачу клиента по созданию ссылок, указывающих на правильный хост, порт и схему.

RFC 7239 определяет HTTP-заголовок Forwarded, который прокси-серверы могут использовать для предоставления информации об исходном запросе. Существуют и другие нестандартные заголовки, включая X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl и X-Forwarded-Prefix.

ForwardedHeaderTransformer - это компонент, который изменяет хост, порт и схему запроса на основе пересылаемых заголовков, а затем удаляет эти заголовки. Если вы объявите его как бин с именем forwardedHeaderTransformer, он будет обнаружен и использован.

Есть некоторые предосторожности касательно безопасности для пересылаемых заголовков, поскольку приложение не может знать, были ли заголовки добавлены прокси-сервером, как предполагалось, или вредоносным клиентом. Именно поэтому прокси-сервер на границе доверия должен быть сконфигурирован на удаление ненадежного пересылаемого трафика, поступающего извне. Вы также можете сконфигурировать ForwardedHeaderTransformer с использованием removeOnly=true, и в этом случае он будет удалять, но не использовать заголовки.

В версии 5.1 ForwardedHeaderFilter устарел и был заменен на ForwardedHeaderTransformer, поэтому пересылаемые заголовки могут обработаны раньше, до создания обмена. Если фильтр все равно сконфигурирован, он удаляется из списка фильтров, а вместо него используется ForwardedHeaderTransformer.

Фильтры

В API WebHandler вы можете использовать WebFilter для применения логики в стиле перехвата перед и после остальной цепочки обработки фильтров и целевого WebHandler. При использовании конфигурации WebFlux зарегистрировать WebFilter крайне просто – объявляем его как бин Spring и (опционально) выражаем уровень старшинства, используя аннотацию @Order в объявлении бина или реализуя класс Ordered.

CORS

Spring WebFlux обеспечивает тонкую поддержку конфигурации CORS с помощью аннотаций для контроллеров. Однако, если вы используете его со Spring Security, то советуем полагаться на встроенный CorsFilter, который должен находиться по порядку впереди всей цепочки фильтров Spring Security.

Exceptions

В API WebHandler вы можете использовать WebExceptionHandler для обработки исключений из цепочки экземпляров WebFilter и целевого WebHandler. При использовании конфигурации WebFlux регистрировать обработчик WebExceptionHandler так же просто, как объявлять его в качестве бина Spring и (опционально) выражать старшинство с помощью аннотации @Order в объявлении бина или путем реализации класса Ordered.

В следующей таблице описаны доступные реализации WebExceptionHandler:

Обработчик исключений Описание

ResponseStatusExceptionHandler

Обеспечивает обработку исключений типа ResponseStatusException путем установки ответа на код состояния HTTP, соответствующий исключению.

WebFluxResponseStatusExceptionHandler

Расширение ResponseStatusExceptionHandler, которое также может определять код состояния HTTP по аннотации @ResponseStatus для любого исключения.

Этот обработчик объявляется в WebFlux Config.

Кодеки

Модули spring-web и spring-core обеспечивают средства поддержки сериализации и десериализации байтового содержимого в объекты более высокого уровня и из них посредством неблокирующего ввода-вывода с обратной реакцией из спецификации Reactive Streams. Ниже описаны эти средства поддержки:

  • Encoder и Decoder — это контракты низкого уровня для кодирования и декодирования содержимого независимо от HTTP протокола.

  • HttpMessageReader и HttpMessageWriter — это контракты для кодирования и декодирования содержимого HTTP-сообщений.

  • Encoder можно обернуть в EncoderHttpMessageWriter, чтобы адаптировать его под использовании в веб-приложении, а Decoder можно обернуть в DecoderHttpMessageReader.

  • DataBuffer абстрагирует различные представления байтовых буферов (например, ByteBuf, java.nio.ByteBuffer для Netty и т.д.), и именно с ним и работают все кодеки.

Модуль spring-core содержит реализации кодера и декодера byte[], ByteBuffer, DataBuffer, Resource и String. Модуль spring-web содержит Jackson JSON, Jackson Smile, JAXB2, Protocol Buffers и другие кодировщики и декодировщики наряду с реализациями веб-ориентированных средств чтения и записи HTTP-сообщений для данных формы, многокомпонентного содержимого, событий, отправляемых сервером, и др.

ClientCodecConfigurer и ServerCodecConfigurer обычно используются, чтобы конфигурировать и настраивать кодеки для использования в приложении.

Jackson JSON

Формат JSON и двоичный JSON (Smile) поддерживаются при наличии библиотеки Jackson.

Jackson2Decoder работает следующим образом:

  • Асинхронный, неблокирующий парсер библиотеки Jackson используется для объединения потока байтовых фрагментов в экземпляры TokenBuffer, каждый из которых представляет собой объект JSON.

  • Каждый TokenBuffer передается в ObjectMapper библиотеки Jackson для создания объекта более высокого уровня.

  • При декодировании в публикатор с одним значением (например, Mono), существует один TokenBuffer.

  • При декодировании в многозначный публикатор (например, Flux) каждый TokenBuffer передается в ObjectMapper, как только будет получено достаточно байтов информации для полностью сформированного объекта. Вводимым содержимым может быть массив JSON или любой формат JSON с разграничением строк, такой как NDJSON, JSON Lines или JSON Text Sequences.

Jackson2Encoder работает следующим образом:

  • Для публикатора с одним значеним (например, Mono), просто сериализуем его через ObjectMapper.

  • Для многозначного публикатора с application/json по умолчанию собираем значения с помощью Flux#collectToList(), а затем сериализуем полученную коллекцию.

  • Для многозначного публикатора с потоковым типом среды передачи данных, таким как application/x-ndjson или application/stream+x-jackson-smile, кодируем, записываем и сбрасываем каждое значение отдельно, используя формат JSON с разграничением строк. В кодировщике могут быть зарегистрированы другие типы потоковых сред передачи данных.

  • В случае SSE (событий, посылаемых сервером) Jackson2Encoder вызывается для каждого события, а вывод сбрасывается, чтобы обеспечить передачу без задержки.

По умолчанию и Jackson2Encoder, и Jackson2Decoder не поддерживают элементы типа String. Вместо этого по умолчанию предполагается, что строка или последовательность строк представляют собой сериализованное JSON-содержимое, которое будет отображаться при помощи CharSequenceEncoder. Если требуется сгенерировать массив JSON из Flux<String>, используйте Flux#collectToList() и закодируйте Mono<List<String>>.

Данные формы

FormHttpMessageReader и FormHttpMessageWriter поддерживают декодирование и кодирование содержимого application/x-www-form-urlencoded.

На стороне сервера, где доступ к содержимому формы зачастую должен осуществляться из нескольких мест, ServerWebExchange предусматривает специальный метод getFormData(), который парсит содержимое через FormHttpMessageReader, а затем кэширует результат для осуществления повторного доступа.

После использования getFormData() исходное сырое содержимое больше нельзя прочитать из тела запроса. По этой причине ожидается, что приложения будут последовательно проходить через ServerWebExchange для получения доступа к кэшированным данным формы вместо чтения из сырого тела запроса.

Многокомпонентность

MultipartHttpMessageReader и MultipartHttpMessageWriter поддерживают декодирование и кодирование содержимого "multipart/form-data". В свою очередь MultipartHttpMessageReader делегирует полномочия другому HttpMessageReader для фактического парсинга в Flux<Part>, а затем просто собирает части в MultiValueMap. По умолчанию используется DefaultPartHttpMessageReader, но это можно изменить с помощью ServerCodecConfigurer. Для получения дополнительной информации о DefaultPartHttpMessageReader, обратитесь к javadoc по DefaultPartHttpMessageReader.

На стороне сервера, где доступ к содержимому многокомпонентной формы может потребоваться осуществлять из нескольких мест, ServerWebExchange предусматривает специальный метод getMultipartData(), который парсит содержимое через MultipartHttpMessageReader, а затем кэширует результат для получения повторного доступа.

После использования параметра getMultipartData() исходное сырое содержимое больше нельзя прочитать из тела запроса. По этой причине приложениям нужно постоянно использовать параметр getMultipartData() для многократного доступа к компонентам в виде Map, или же полагаться на SynchronossPartHttpMessageReader для единовременного доступа к Flux<Part>.

Ограничения

Реализации Decoder и HttpMessageReader, которые буферизируют часть или весь поток вводных данных, могут быть сконфигурированы с ограничением на максимальное количество байт для буферизации в памяти. В некоторых случаях буферизация происходит потому, что вводные данные агрегируются и представляются как единый объект – например, метод контроллера с аннотацией @RequestBody byte[], x-www-form-urlencoded данные и так далее. Буферизация также может возникать при потоковой передаче, если происходит разделение вводного потока – например, текст с разделителями, поток объектов в формате JSON и так далее. Для этих случаев потоковой передачи ограничение применяется к количеству байт, связанных с одним объектом в потоке.

Для конфигурирования размеров буферов можно проверить, открывает ли данный Decoder или HttpMessageReader свойство maxInMemorySize, и если да, то в Javadoc будут содержаться подробности о значениях по умолчанию. На стороне сервера ServerCodecConfigurer предусматривает единое место, откуда можно установить все кодеки. На стороне клиента ограничение для всех кодеков можно изменить вWebClient.Builder.

Для многокомпонентного парсинга свойство maxInMemorySize ограничивает размер нефайловых компонентов. Для файловых компонентов оно определяет порог, при котором компонент записывается на диск. Для файловых компонентов, записанных на диск, существует дополнительное свойство maxDiskUsagePerPart, ограничивающее объем дискового пространства для каждого компонента. Существует также свойство maxParts для ограничения общего количества компонентов в многокомпонентном запросе. Чтобы сконфигурировать все три компонента в WebFlux, нужно указать предварительно сконфигурированный экземпляр MultipartHttpMessageReader в ServerCodecConfigurer.

Потоковая передача

При потоковой передаче данных в HTTP-ответ (например, text/event-stream, application/x-ndjson) важно передавать данные периодически, чтобы как можно раньше точно обнаружить отключенный клиент. Так передавать можно только комментарий, пустое SSE-событие или любые другие данные "пустые операции", которые эффективно послужат в качестве heartbeat-сообщения.

DataBuffer

DataBuffer - это представление для байтового буфера в WebFlux. Важно понимать, что на некоторых серверах, таких как Netty, байтовые буферы объединены в пул и подсчитываются по ссылкам, и должны быть освобождены при потреблении, чтобы избежать утечки памяти.

В случае приложений на WebFlux обычно не требуется беспокоиться о таких проблемах, если только они не потребляют и не производят буферы данных напрямую вместо использования кодеков для преобразования в объекты более высокого уровня и обратно, или если они не создают собственные кодеки.

Журналирование

Журналирование на уровне DEBUG в Spring WebFlux спроектировано так, чтобы быть компактным, простым и удобным для человека. Оно сосредоточено на наиболее значимых битах информации, которые будут использоваться снова и снова, в отличие от других, которые используются только при отладке конкретной проблемы.

Журналирование на уровне TRACE в целом следует тем же принципам, что и DEBUG (и, например, также не должно быть перегруженным), но может использоваться для отладки любой проблемы. Кроме того, некоторые сообщения журнала могут демонстрировать разный уровень детализации на уровнях TRACE и DEBUG.

Надлежащее журналирование зависит от опыта использования журналов. Если вы заметите что-то, что не соответствует заявленным целям, пожалуйста, сообщите нам об этом.

Идентификатор журнала

В WebFlux один запрос может выполняться в нескольких потоках, а идентификатор потока бесполезен для сопоставления сообщений журнала, относящихся к конкретному запросу. Именно поэтому сообщения журнала WebFlux по умолчанию имеют префикс с идентификатором конкретного запроса.

На стороне сервера идентификатор журнала хранится в атрибуте ServerWebExchange (LOG_ID_ATTRIBUTE), а полностью отформатированный префикс на основе этого идентификатора доступен из ServerWebExchange#getLogPrefix(). На стороне WebClient идентификатор журнала хранится в атрибуте ClientRequest (LOG_ID_ATTRIBUTE), а полностью отформатированный префикс доступен из ClientRequest#logPrefix().

Конфиденциальные данные

Журнал на уровнях DEBUG и TRACE может регистрировать конфиденциальную информацию. Именно поэтому параметры и заголовки форм по умолчанию маскируются, и вам необходимо явно активировать их полное протоколирование.

В следующем примере показано, как это сделать для запросов на стороне сервера:

Java
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
Kotlin
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}

В следующем примере показано, как это сделать для запросов на стороне клиента:

Java
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
Kotlin
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()

Appenders

Библиотеки логирования, такие как SLF4J и Log4J 2, предоставляют асинхронные логгеры, которые позволяют избежать блокировки. Хотя они имеют свои недостатки, такие как потенциальный пропуск сообщений, которые нельзя поставить в очередь на логирование, они являются лучшими доступными вариантами для использования в реактивных, неблокирующих приложениях.

Кастомные кодеки

Приложения могут регистрировать кастомные кодеки для поддержки дополнительных типов серды передачи данных или специфической логики работы, которые не поддерживаются кодеками по умолчанию.

Некоторые параметры конфигурации, выраженные разработчиками, применяются к кодекам по умолчанию. Пользовательским кодекам, вероятно, потребуется получить возможность согласовываться с этими настройками, например, принудительно ограничивать буферизацию или регистрировать конфиденциальные данные.

В следующем примере показано, как это сделать для запросов на стороне клиента:

Java
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
        CustomDecoder decoder = new CustomDecoder();
        configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
Kotlin
val webClient = WebClient.builder()
.codecs({ configurer ->
        val decoder = CustomDecoder()
        configurer.customCodecs().registerWithDefaultConfig(decoder)
 })
.build()