-
Для обработки запросов сервера существует два уровня средств поддержки.
-
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 |
|
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
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
Tomcat
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();
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
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();
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> |
|
0..N |
Обеспечивает обработку исключений из цепочки экземпляров |
<any> |
|
0..N |
Применяет логику в стиле перехвата перед и после остальной цепочки фильтров и целевого |
|
|
1 |
Обработчик запроса. |
|
|
0..1 |
Диспетчер для экземпляров |
|
|
0..1 |
Обеспечивает доступ к экземплярам |
|
|
0..1 |
Распознаватель для |
|
|
0..1 |
Предназначен для обработки заголовков пересылаемых типов либо путем их извлечения и удаления, либо только путем их удаления. По умолчанию не используется. |
Данные формы
ServerWebExchange
предоставляет следующий метод для доступа к данным формы:
Mono<MultiValueMap<String, String>> getFormData();
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
предоставляет следующий метод для доступа к многокомпонентным данным:
Mono<MultiValueMap<String, Part>> getMultipartData();
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
, и в этом случае он будет удалять, но не использовать заголовки.
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
:
Обработчик исключений | Описание |
---|---|
|
Обеспечивает обработку исключений типа |
|
Расширение Этот обработчик объявляется в 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
может регистрировать конфиденциальную информацию. Именно поэтому параметры и заголовки форм по умолчанию маскируются, и вам необходимо явно активировать их полное протоколирование.
В следующем примере показано, как это сделать для запросов на стороне сервера:
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}
В следующем примере показано, как это сделать для запросов на стороне клиента:
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();
val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()
Appenders
Библиотеки логирования, такие как SLF4J и Log4J 2, предоставляют асинхронные логгеры, которые позволяют избежать блокировки. Хотя они имеют свои недостатки, такие как потенциальный пропуск сообщений, которые нельзя поставить в очередь на логирование, они являются лучшими доступными вариантами для использования в реактивных, неблокирующих приложениях.
Кастомные кодеки
Приложения могут регистрировать кастомные кодеки для поддержки дополнительных типов серды передачи данных или специфической логики работы, которые не поддерживаются кодеками по умолчанию.
Некоторые параметры конфигурации, выраженные разработчиками, применяются к кодекам по умолчанию. Пользовательским кодекам, вероятно, потребуется получить возможность согласовываться с этими настройками, например, принудительно ограничивать буферизацию или регистрировать конфиденциальные данные.
В следующем примере показано, как это сделать для запросов на стороне клиента:
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ