Spring Web Reactive | 1. Spring WebFlux | 1.2. Reactive Core

spring-webモジュールには、リアクティブWebアプリケーション向けの次の基本的なサポートが含まれています。

  • サーバーリクエスト処理には、2つのレベルのサポートがあります。

    • HttpHandler: Reactor Netty、Undertow、Tomcat、JettyなどのServlet 3.1+コンテナ向けアダプターとともに、ノンブロッキングI/OとReactive Streamsのバックプレッシャーを使用してHTTPリクエストを処理する基本的な契約です。

    • WebHandler API: リクエスト処理のための、やや高レベルな汎用Web APIです。その上に、アノテーション付きコントローラーや関数型エンドポイントなどの具体的なプログラミングモデルが構築されます。

  • クライアント側には、ノンブロッキングI/OとReactive Streamsのバックプレッシャーを使用してHTTPリクエストを実行するための基本的なClientHttpConnector契約と、Reactor Netty、リアクティブなJetty HttpClientApache HttpComponents向けのアダプターがあります。アプリケーションで使用される高レベルなWebClientは、この契約を基盤として構築されています。

  • クライアントおよびサーバーでは、HTTPリクエストとレスポンスの内容をserializeおよびdeserializeするためのコーデック

1.2.1. HttpHandler

HttpHandlerは、リクエストとレスポンスを処理する単一メソッドを持つ単純な契約です。これは意図的に最小化されており、主な唯一の目的は、さまざまなHTTPサーバーAPIを最小限に抽象化することです。

次の表は、サポートされるサーバーAPIを示します。

サーバー名 使用されるサーバーAPI Reactive Streamsサポート
Netty Netty API Reactor Netty
Undertow Undertow API spring-web: UndertowからのReactive Streamsブリッジ
Tomcat Servlet 3.1 non-blocking I/O。ByteBuffersとbyte[]を読み書きするTomcat API spring-web: Servlet 3.1 non-blocking I/OからのReactive Streamsブリッジ
Jetty Servlet 3.1 non-blocking I/O。ByteBuffersとbyte[]を書き込むJetty API spring-web: Servlet 3.1 non-blocking I/OからのReactive Streamsブリッジ
Servlet 3.1コンテナ Servlet 3.1 non-blocking I/O spring-web: Servlet 3.1 non-blocking I/OからのReactive Streamsブリッジ

次の表は、サーバーの依存関係を示します(サポートされるバージョンを参照)。

サーバー名 グループId Artifact名
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

次のコード断片は、各サーバーAPIでHttpHandlerアダプターを使用する方法を示しています。

Reactor Netty

Java

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

Kotin

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

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();

Kotin

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();

Kotin

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+コンテナ

Servlet 3.1+コンテナにWARとしてデプロイするには、AbstractReactiveWebInitializerを継承してWARに含めることができます。このクラスはHttpHandlerServletHttpHandlerAdapterでラップし、それをServletとして登録します。

1.2.2. WebHandler API

org.springframework.web.serverパッケージは、HttpHandlerを基盤として、複数のWebExceptionHandler、複数のWebFilter、単一のWebHandlerコンポーネントのチェーンを通じてリクエストを処理するための汎用Web APIを提供します。チェーンコンポーネントを自動検出するSpring ApplicationContextを指定するか、コンポーネントをビルダーに登録するだけで、WebHttpHandlerBuilderと組み合わせることができます。

HttpHandlerにはさまざまなHTTPサーバーの使用を抽象化する目的がありますが、WebHandler APIは、次のようなWebアプリケーションで一般的に使用される幅広い機能セットを提供することを目的としています。

  • 属性を持つユーザーセッション。
  • リクエスト属性。
  • リクエストのLocaleまたはPrincipalの解決。
  • 解析されキャッシュされたフォームデータへのアクセス。
  • マルチパートデータ抽象化。
  • その他。

特別なBean型

次の表は、WebHttpHandlerBuilderがSpring ApplicationContextから自動検出できるコンポーネント、または直接登録できるコンポーネントの一覧です。

Bean名 Bean型 カウント 説明
<any> WebExceptionHandler 0..N WebFilterインスタンスのチェーンおよび対象のWebHandlerからの例外処理を提供します。詳細は例外を参照してください。
<any> WebFilter 0..N フィルターチェーンの残りと対象のWebHandlerの前後に、インターセプトスタイルのロジックを適用します。詳細はフィルターを参照してください。
webHandler WebHandler 1 リクエストハンドラー。
webSessionManager WebSessionManager 0..1 ServerWebExchangeのメソッドを通じて公開されるWebSessionインスタンスのマネージャー。デフォルトはDefaultWebSessionManagerです。
serverCodecConfigurer ServerCodecConfigurer 0..1 フォームデータとマルチパートデータを解析するためのHttpMessageReaderインスタンスへのアクセス。このデータはServerWebExchangeのメソッドを通じて公開されます。デフォルトはServerCodecConfigurer.create()です。
localeContextResolver LocaleContextResolver 0..1 ServerWebExchangeのメソッドを通じて公開されるLocaleContextのリゾルバー。デフォルトはAcceptHeaderLocaleContextResolverです。
forwardedHeaderTransformer ForwardedHeaderTransformer 0..1 Forwarded系ヘッダーを処理するために、抽出して削除するか、破棄します。デフォルトでは使用されません。

フォームデータ(Form Data)

ServerWebExchangeは、フォームデータにアクセスするために次のメソッドを提供します。

Java

Mono<MultiValueMap<String, String>> getFormData();

Kotin

suspend fun getFormData(): MultiValueMap<String, String>

DefaultServerWebExchangeは、構成されたHttpMessageReaderを使用してフォームデータ(application/x-www-form-urlencoded)を解析し、MultiValueMapにします。デフォルトでは、FormHttpMessageReaderServerCodecConfigurer Beanを使用するように構成されています(WebHandler APIを参照)。

マルチパートデータ(Multipart Data)

Web MVC

ServerWebExchangeは、マルチパートデータにアクセスするために次のメソッドを提供します。

Java

Mono<MultiValueMap<String, Part>> getMultipartData();

Kotin

suspend fun getMultipartData(): MultiValueMap<String, Part>

DefaultServerWebExchangeは、構成されたHttpMessageReader<MultiValueMap<String, Part>>を使用して、multipart/form-dataコンテンツをMultiValueMapに解析します。デフォルトはDefaultPartHttpMessageReaderで、サードパーティ依存関係はありません。または、Synchronoss NIO Multipartライブラリに基づくSynchronossPartHttpMessageReaderを使用できます。いずれもServerCodecConfigurer Beanを通じて構成されます(WebHandler APIを参照)。

ストリーミング形式でマルチパートデータを解析するには、HttpMessageReader<Part>から返されるFlux<Part>を使用します。たとえば、アノテーション付きコントローラーで@RequestPartを使用すると、Mapのように名前で各パートにアクセスすることを意味するため、マルチパートデータを完全に解析する必要があります。対照的に、@RequestBodyを使用すると、MultiValueMapに収集せずにコンテンツをFlux<Part>へデコードできます。

Forwardedヘッダー

Web MVC

リクエストがプロキシ(ロードバランサーなど)を通ると、ホスト、ポート、スキームが変更されることがあります。そのため、クライアントの観点から正しいホスト、ポート、スキームを指すリンクを作成することが難しくなります。

RFC 7239は、プロキシが元のリクエストに関する情報を提供するために使用できるForwarded HTTPヘッダーを定義しています。X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefixなど、他の非標準ヘッダーもあります。

ForwardedHeaderTransformerは、Forwardedヘッダーに基づいてリクエストのホスト、ポート、スキームを変更し、そのヘッダーを削除するコンポーネントです。forwardedHeaderTransformerというBeanとして宣言すると、自動検出されて使用できます。

Forwardedヘッダーには、ヘッダーが意図どおりプロキシによって追加されたのか、悪意のあるクライアントによって追加されたのかをアプリケーションが判別できないというセキュリティ上の考慮事項があります。そのため、信頼境界にあるプロキシで、外部からの信頼できないForwardedトラフィックを削除するように構成する必要があります。ForwardedHeaderTransformerremoveOnly=trueで構成できます。この場合、ヘッダーは使用されず削除されます。

5.1では、exchangeが作成される前にForwardedヘッダーをより早く処理できるように、ForwardedHeaderFilterは使用されなくなり、ForwardedHeaderTransformerに置き換えられました。フィルターが構成されている場合はフィルター一覧から除外され、代わりにForwardedHeaderTransformerが使用されます。

5.1では、ForwardedHeaderFilterはdeprecatedとなり、Forwardedヘッダーをより早く処理できるようにForwardedHeaderTransformerへ置き換えられました。フィルターが構成されている場合はフィルター一覧から除外され、代わりにForwardedHeaderTransformerが使用されます。

1.2.3. フィルター(Filters)

Web MVC

WebHandler APIは、WebFilterを使用して、フィルターチェーンの残りと対象のWebHandlerの前後にインターセプションロジックを適用できます。WebFlux構成を使用する場合、WebFilterの登録はSpring Beanとして宣言するだけでよく、必要に応じてBean宣言に@Orderを使用するかOrderedを実装して優先順位を設定できます。

CORS

Web MVC

Spring WebFluxは、コントローラーのアノテーションを通じてCORS構成の詳細なサポートを提供します。ただし、Spring Securityと併用する場合は、Spring Securityフィルターチェーンより前に並べる必要がある組み込みのCorsFilterを使用することを推奨します。

詳細は、CORSおよびwebflux-cors.htmlセクションを参照してください。

1.2.4. 例外(Exceptions)

Web MVC

WebHandler APIは、WebExceptionHandlerを使用して、WebFilterインスタンスのチェーンおよび対象のWebHandlerからの例外を処理できます。WebFlux構成を使用する場合、WebExceptionHandlerの登録はSpring Beanとして宣言するだけでよく、必要に応じてBean宣言に@Orderを使用するかOrderedを実装して優先順位を設定できます。

次の表では、使用可能なWebExceptionHandler実装について説明します。

例外ハンドラー 説明
ResponseStatusExceptionHandler 例外のHTTPステータスコードをレスポンスに設定することで、ResponseStatusExceptionの例外処理を提供します。
WebFluxResponseStatusExceptionHandler @ResponseStatusアノテーションのHTTPステータスコードも例外から決定できるResponseStatusExceptionHandlerの拡張です。このハンドラーはWebFlux構成で宣言できます。

1.2.5. コーデック(Codecs)

Web MVC

spring-webおよびspring-coreモジュールは、Reactive Streamsのバックプレッシャーを使用したノンブロッキングI/Oを通じて、高レベルオブジェクトとの間でバイトコンテンツをシリアライズおよびデシリアライズするサポートを提供します。このサポートについて以下で説明します。

  • EncoderDecoderは、HTTPに依存せずコンテンツをエンコードおよびデコードします。
  • HttpMessageReaderHttpMessageWriterは、HTTPメッセージコンテンツをエンコードおよびデコードします。
  • EncoderEncoderHttpMessageWriterで、DecoderDecoderHttpMessageReaderでラップすることで、Webアプリケーションでの使用に適用できます。
  • DataBufferは、Netty ByteBufjava.nio.ByteBufferなど、さまざまなバイトバッファ表現を抽象化し、すべてのコーデックがその上で動作します。このトピックの詳細は、「Spring Core」セクションのData Buffers and Codecsを参照してください。

spring-coreモジュールは、byte[]ByteBufferDataBufferResourceStringのエンコーダーおよびデコーダー実装を提供します。spring-webモジュールは、Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers、その他のエンコーダーとデコーダー、フォームデータ、マルチパートコンテンツ、Server-Sent EventsなどのWeb専用HTTPメッセージReaderおよびWriter実装を提供します。

ClientCodecConfigurerおよびServerCodecConfigurerは、通常、アプリケーションで使用するコーデックの構成とカスタマイズに使用されます。HTTP message codecs設定セクションを参照してください。

Jackson JSON

JSONおよびバイナリJSON(Smile)は、Jacksonライブラリが存在する場合にサポートされます。

Jackson2Decoderは次のように動作します。

  • Jacksonの非同期ノンブロッキングパーサーは、バイトチャンクのストリームを、それぞれJSONオブジェクトを表すTokenBufferへ統合するために使用されます。
  • TokenBufferはJacksonのObjectMapperに渡され、より高レベルのオブジェクトを作成します。
  • 単一値パブリッシャー(例: Mono)へデコードする場合、TokenBufferは1つです。
  • 複数値パブリッシャー(Fluxなど)へデコードする場合、完全なオブジェクトを形成するのに十分なバイトを受信するとすぐに、各TokenBufferObjectMapperへ渡されます。入力内容はJSON配列、またはNDJSON、JSON Lines、JSON Text Sequenceなどの行区切り(line-delimited)JSON形式にできます。

Jackson2Encoderは次のように動作します。

  • 単一値パブリッシャー(例: Mono)の場合は、単純にObjectMapperを通じてシリアライズします。
  • application/jsonを使用する複数値パブリッシャーの場合、デフォルトではFlux#collectToList()を使用して値を収集し、結果のコレクションをシリアライズします。
  • application/x-ndjsonapplication/stream+x-jackson-smileなどのストリーミングメディアタイプを持つ複数値パブリッシャーの場合、行区切り(line-delimited)JSON形式を使用して、値を個別にエンコード、書き込み、flushします。他のストリーミングメディアタイプもエンコーダーに登録できます。
  • SSEの場合、Jackson2Encoderはイベントごとに呼び出され、出力はflushされて遅延なく配信されます。

デフォルトでは、Jackson2EncoderJackson2Decoderはいずれも型Stringの要素をサポートしません。代わりに、文字列または文字列のシーケンスは、デフォルトでCharSequenceEncoderによってレンダリングされるシリアライズ済みJSONコンテンツを表します。Flux<String>からJSON配列をレンダリングする必要がある場合は、Flux#collectToList()を使用してMono<List<String>>をエンコードします。

フォームデータ(Form Data)

FormHttpMessageReaderFormHttpMessageWriterは、application/x-www-form-urlencodedコンテンツのデコードおよびエンコードをサポートします。

サーバー側ではフォームコンテンツに複数箇所からアクセスすることが多いため、ServerWebExchangeFormHttpMessageReaderを通じてコンテンツを解析し、繰り返し使用するために結果をキャッシュする専用のgetFormData()メソッドを提供します。WebHandler APIセクションのフォームデータを参照してください。

getFormData()を使用すると、元のコンテンツはリクエスト本文からそれ以上読み取れません。したがって、アプリケーションは生のリクエスト本文(request body)から読むのではなく、キャッシュされたフォームデータへアクセスするためにServerWebExchangeを経由する必要があります。

マルチパート(Multipart)

MultipartHttpMessageReaderMultipartHttpMessageWriterは、multipart/form-dataコンテンツのデコードとエンコードをサポートします。MultipartHttpMessageReaderは別のHttpMessageReaderへ委譲してFlux<Part>へ解析し、その結果をMultiValueMapへ収集します。デフォルトではDefaultPartHttpMessageReaderが使用されますが、これはServerCodecConfigurerを通じて変更できます。DefaultPartHttpMessageReaderの詳細は、DefaultPartHttpMessageReaderのjavadocを参照してください。

サーバー側ではマルチパートフォームコンテンツに複数箇所からアクセスできるため、ServerWebExchangeMultipartHttpMessageReaderを通じてコンテンツを解析し、繰り返し使用するために結果をキャッシュする専用のgetMultipartData()メソッドを提供します。WebHandler APIセクションのマルチパートデータを参照してください。

getMultipartData()を使用すると、元の生コンテンツはリクエスト本文から読み取れません。したがって、アプリケーションはmapのように繰り返しパートへアクセスするためにgetMultipartData()を一貫して使用するか、Flux<Part>への1回限りのアクセスのためにSynchronossPartHttpMessageReaderに依存する必要があります。

制限(Limits)

入力ストリームの一部または全部をバッファリングするDecoderおよびHttpMessageReader実装では、メモリにバッファリングする最大バイト数の制限を設定できます。バッファリングは、入力が1つの集約オブジェクトとして表現されるために発生する場合があります。たとえば、@RequestBody byte[]x-www-form-urlencodedデータを扱うコントローラーメソッドです。また、バッファリングされた入力ストリームを分割するストリーミングでも発生することがあります。たとえば、区切り付きテキストやJSONオブジェクトのストリームです。このようなストリーミングの場合、制限はストリーム内の1つのオブジェクトに関連付けられたバイトに適用されます。

バッファサイズを設定するには、特定のDecoderまたはHttpMessageReadermaxInMemorySizeプロパティを公開しているか確認し、公開している場合はJavadocにデフォルト値の詳細が含まれているか確認します。サーバー側では、ServerCodecConfigurerがすべてのコーデックを設定するための一箇所の場所を提供します。HTTP message codecsを参照してください。クライアント側では、すべてのコーデックの制限をWebClient.Builderで変更できます。

マルチパート解析の場合、maxInMemorySizeプロパティは非ファイルパートのサイズを制限します。ファイルパートの場合は、パートがディスクへ書き込まれるしきい値を決定します。ディスクに書き込まれるファイルパートには、パートごとのディスク使用量を制限する追加のmaxDiskUsagePerPartプロパティがあります。マルチパートリクエストのパート数を制限するmaxPartsプロパティもあります。WebFluxでこの3つすべてを構成するには、構成済みのMultipartHttpMessageReaderServerCodecConfigurerのインスタンスを提供する必要があります。

ストリーミング(Streaming)

Web MVC

HTTPレスポンス(たとえばtext/event-streamapplication/x-ndjson)へストリーミングする場合、切断されたクライアントをできるだけ早く検出するために、定期的にデータを送信することが重要です。このような送信は、短い文字列、空のSSEイベント、またはハートビートとして効果的に機能するその他の「no-op」データにできます。

DataBuffer

DataBufferはWebFluxにおけるバイトバッファの表現です。このリファレンスのSpring Core部分では、Data Buffers and Codecsセクションで詳しく説明されています。理解すべき重要な点は、Nettyなど一部のサーバーでは、バイトバッファがプールされ参照カウントされており、メモリリークを防ぐために消費(consume)され、必ずリリースされなければならないということです。

WebFluxアプリケーションは、コーデックを通じて高レベルオブジェクトとの変換を行わずにデータバッファを直接消費または生成しない限り、またはカスタムコーデックを作成することを選択しない限り、通常このような課題を考慮する必要はありません。そのような場合は、Data Buffers and Codecsの情報、特にDataBufferの使用セクションを確認してください。

1.2.6. ロギング

Web MVC

Spring WebFluxのDEBUGレベルログは、軽量で最小限かつ人が読みやすいように設計されています。これは、特定の問題をデバッグする場合にのみ役立つ情報ではなく、継続的に役立つ情報に重点を置いています。

TRACEレベルのログは、一般的にDEBUGと同じ原則に従います(たとえば、firehoseのようになってはいけません)。ただし、問題のデバッグに使用できます。また、一部のログメッセージはTRACEDEBUGで詳細レベルが異なる場合があります。

良いログは、ログを使用した経験から得られます。記載された目的と異なるものを見つけた場合は、お知らせください。

ログID

WebFluxでは、単一のリクエストが複数のスレッドで実行されることがあります。スレッドIDは、特定のリクエストに属するログメッセージの相関を見つけるにはあまり役立ちません。そのため、デフォルトではWebFluxログメッセージの先頭にリクエスト固有のIDが付きます。

サーバー側では、ログIDはServerWebExchange属性(LOG_ID_ATTRIBUTE)に保存されますが、そのIDに基づく完全にフォーマットされた接頭辞はServerWebExchange#getLogPrefix()で使用できます。WebClientでは、ログIDはClientRequest属性(LOG_ID_ATTRIBUTE)に保存されますが、完全にフォーマットされた接頭辞はClientRequest#logPrefix()で取得できます。

機密データ

Web MVC

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やLog4J2などのロギングライブラリは、ブロッキングを避けるために非同期ロガーを提供します。ロギングのためにキューへ入れられないメッセージを破棄するなど独自の欠点はありますが、現在のリアクティブでノンブロッキングなアプリケーションで使用できる最善の選択肢です。

カスタムコーデック

アプリケーションは、追加のメディアタイプをサポートするカスタムコーデック、またはデフォルトコーデックでサポートされていない特定の動作を登録できます。

開発者に公開されている一部の構成オプションは、デフォルトコーデックに適用されます。カスタムコーデックは、バッファリング制限の強制や機密データロギングなどの設定に合わせて調整する機会を得たい場合があります。

次の例は、クライアント側リクエストに対してカスタムコーデックを設定する方法を示しています。

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()