ドキュメントのこのパートでは、Netty、Undertow、Servlet 3.1+ コンテナーなどのノンブロッキングサーバーで実行する Reactive Streams (英語) API 上に構築されたリアクティブスタック Web アプリケーションのサポートについて説明します。個々の章では、Spring WebFlux フレームワーク、リアクティブ WebClientテストのサポート、およびリアクティブライブラリについて説明しています。サーブレットスタック Web アプリケーションについては、Web MVC サーブレットスタックを参照してください。

1. Spring WebFlux

Spring Framework、Spring Web MVC に含まれる元の Web フレームワークは、サーブレット API およびサーブレットコンテナー専用に作成されました。リアクティブスタック Web フレームワーク Spring WebFlux は、バージョン 5.0 で後から追加されました。完全にノンブロッキングで、Reactive Streams (英語) バックプレッシャーをサポートし、Netty、Undertow、Servlet 3.1+ コンテナーなどのサーバーで実行されます。

両方の Web フレームワークは、ソースモジュール(spring-webmvc (GitHub) および spring-webflux (GitHub) )の名前を反映し、Spring Framework で共存します。各モジュールはオプションです。アプリケーションは、一方または他方のモジュール、または場合によっては両方を使用できます。たとえば、リアクティブ WebClient を備えた Spring MVC コントローラーです。

1.1. 概要

Spring WebFlux が作成されたのはなぜですか?

答えの 1 つは、少ないスレッドで同時実行 (Qiita) し、より少ないハードウェアリソースでスケーリング (英語) するためのノンブロッキング Web スタックの必要性です。Servlet 3.1 は、ノンブロッキング I/O 用の API を提供しました。ただし、これを使用すると、契約が同期(FilterServlet)またはブロッキング(getParametergetPart)であるサーブレット API の残りの部分から離れてしまうことになります。これが、ノンブロッキングランタイム全体の基盤として機能する新しい共通 API の動機でした。これは、非同期、ノンブロッキングの領域で十分に確立されている (Netty のような) サーバーのために重要です。

もう 1 つの理由は関数型プログラミングです。Java 5 にアノテーションが追加されたことで新たな可能性(アノテーション付き REST コントローラーや単体テストなど)を切り開いたように、Java 8 にラムダ式を追加することにより Java の関数 API の可能性を開きました。これは、非同期ロジックの宣言的な合成を可能にする、ノンブロッキングアプリケーションと継続渡しスタイル API(CompletableFuture および ReactiveX (英語) によって普及した)の恩恵です。プログラミングモデルレベルでは、Java 8 により Spring WebFlux はアノテーション付きコントローラーとともに関数 Web エンドポイントを提供できるようになりました。

1.1.1. 「リアクティブ」を定義する

「ノンブロッキング」と「関数型」に触れましたが、リアクティブとはどういう意味でしょうか?

「リアクティブ」という用語は、変化への反応に基づいて構築されたプログラミングモデルを指します。ネットワークコンポーネントは I/O イベントに反応し、UI コントローラーはマウスイベントに反応します。その意味で、ノンブロッキングはリアクティブです。なぜなら、ブロックされる代わりに、操作が完了するかデータが利用可能になると通知に反応するモードになっているからです。

Spring チームが「リアクティブ」に関連付けるもう 1 つの重要なメカニズムは、ノンブロッキングバックプレッシャーです。同期命令型コードでは、呼び出しをブロックすることは、呼び出し側を待機させるバックプレッシャーの自然な形として機能します。ノンブロッキングコードでは、高速プロデューサーがその宛先を圧倒しないように、イベントのレートを制御することが重要になります。

Reactive Streams は、バックプレッシャーのある非同期コンポーネント間の相互作用を定義する小さな仕様 (GitHub) (Java 9 でも採用 )です。たとえば、データリポジトリ(パブリッシャー (英語) として機能)は、HTTP サーバー(サブスクライバー (英語) として機能)がレスポンスに書き込むことができるデータを生成できます。Reactive Streams の主な目的は、サブスクライバーがパブリッシャーがデータを生成する速度と速度を制御できるようにすることです。

よくある質問 : パブリッシャーがスローダウンできない場合はどうなるでしょうか?
Reactive Streams の目的は、メカニズムと境界を確立することのみです。パブリッシャーがスローダウンできない場合、バッファーするか、ドロップするか、失敗するかを決定する必要があります。

1.1.2. リアクティブ API

Reactive Streams は相互運用性に重要なロールを果たします。ライブラリやインフラストラクチャコンポーネントにとっては興味深いものですが、アプリケーション API としては低レベルすぎるため、あまり有用ではありません。アプリケーションには、Java 8 Stream API に似ていますが、コレクションだけでなく、非同期ロジックを構成するために、より高レベルでより関数的な API が必要です。これがリアクティブライブラリが果たすロールです。

Reactor (GitHub) は、Spring WebFlux に最適なリアクティブライブラリです。ReactiveX 演算子のボキャブラリー (英語) に合わせた豊富な演算子セットを通じて、0..1(Mono)および 0..N(Flux)のデータシーケンスで動作する Mono (英語) および Flux (英語) API タイプを提供します。Reactor は Reactive Streams ライブラリであるため、すべてのオペレーターがノンブロッキングバックプレッシャーをサポートしています。Reactor は、サーバー側の Java に重点を置いています。Spring との緊密なコラボレーションで開発されます。

WebFlux は、コア依存として Reactor を必要としますが、Reactive Streams を介して他のリアクティブライブラリと相互運用できます。一般的なルールとして、WebFlux API は入力としてプレーン Publisher を受け入れ、それを内部で Reactor タイプに適合させ、それを使用して、出力として Flux または Mono を返します。任意の Publisher を入力として渡すことができ、出力に操作を適用できますが、別のリアクティブライブラリで使用するために出力を調整する必要があります。可能な場合はいつでも(たとえば、アノテーション付きコントローラー)、WebFlux は RxJava または別のリアクティブライブラリの使用に透過的に適応します。詳細については、リアクティブライブラリを参照してください。

Reactive API に加えて、WebFlux は Kotlin のコルーチン API でも使用でき、より命令的なスタイルのプログラミングを提供します。次の Kotlin コードサンプルは、Coroutines API で提供されます。

1.1.3. プログラミングモデル

spring-web モジュールには、HTTP 抽象化を含む Spring WebFlux、サポートされるサーバー用の Reactive Streams アダプターコーデック、およびサーブレット API に匹敵するがノンブロッキング契約を含むコア WebHandler API の基礎となるリアクティブ基盤が含まれています。

その基盤で、Spring WebFlux は 2 つのプログラミングモデルの選択肢を提供します。

  • アノテーション付きコントローラー : Spring MVC と一致し、spring-web モジュールからの同じアノテーションに基づいています。Spring MVC コントローラーと WebFlux コントローラーは両方とも、リアクティブ(Reactor および RxJava)戻り型をサポートしているため、区別するのは簡単ではありません。注目すべき違いの 1 つは、WebFlux がリアクティブ @RequestBody 引数もサポートしていることです。

  • 関数エンドポイント : ラムダベースの軽量で関数プログラミングモデル。これは、アプリケーションがリクエストのルーティングと処理に使用できる小さなライブラリまたはユーティリティのセットと考えることができます。アノテーション付きコントローラーとの大きな違いは、アプリケーションが開始から終了までのリクエスト処理を担当することと、アノテーションを介してインテントを宣言してコールバックすることです。

1.1.4. 適用性

Spring MVC または WebFlux ?

当然の質問ですが、あやふやな二分法を設定する質問です。実際には、両方が連携して、使用可能なオプションの範囲を拡張します。この 2 つは、相互に継続性と一貫性を保つように設計されており、並行して使用できます。また、双方からのフィードバックは双方に利益をもたらします。次の図は、この 2 つがどのように関連しているか、共通しているもの、およびそれぞれが一意にサポートするものを示しています。

spring mvc and webflux venn

次の特定の点を考慮することをお勧めします。

  • 正常に動作する Spring MVC アプリケーションがある場合、変更する必要はありません。命令型プログラミングは、コードを記述、理解、およびデバッグする最も簡単な方法です。歴史的に、ほとんどがブロックしているため、ライブラリの選択肢は最大限にあります。

  • 既にノンブロッキング Web スタックを購入している場合、Spring WebFlux はこのスペースの他のユーザーと同じ実行モデルの利点を提供し、サーバー(Netty、Tomcat、Jetty、Undertow、および Servlet 3.1+ コンテナー)の選択も提供します。プログラミングモデル(アノテーション付きコントローラーおよび関数 Web エンドポイント)、およびリアクティブライブラリ(Reactor、RxJava、またはその他)の選択。

  • Java 8 ラムダまたは Kotlin で使用する軽量で関数 Web フレームワークに興味がある場合は、Spring WebFlux 関数 Web エンドポイントを使用できます。また、透明性と制御性を高めることでメリットが得られる複雑な要件を持たない小規模なアプリケーションやマイクロサービスにも適しています。

  • マイクロサービスアーキテクチャでは、Spring MVC または Spring WebFlux コントローラーまたは Spring WebFlux 関数エンドポイントのいずれかのアプリケーションを混在させることができます。両方のフレームワークで同じアノテーションベースのプログラミングモデルをサポートしているため、適切なジョブに適切なツールを選択しながら、知識を再利用しやすくなります。

  • アプリケーションを評価する簡単な方法は、依存関係をチェックすることです。使用するブロッキング永続性 API(JPA、JDBC)またはネットワーク API がある場合、Spring MVC は少なくとも一般的なアーキテクチャに最適です。Reactor と RxJava の両方で別のスレッドでブロッキング呼び出しを実行することは技術的には可能ですが、ノンブロッキング Web スタックを最大限に活用することはできません。

  • リモートサービスを呼び出す Spring MVC アプリケーションがある場合は、リアクティブ WebClient を試してください。Spring MVC コントローラーメソッドから直接、リアクティブ型(Reactor、RxJava、またはその他)を返すことができます。コールごとのレイテンシーまたはコール間の相互依存性が大きいほど、メリットは劇的になります。Spring MVC コントローラーは、他のリアクティブコンポーネントも呼び出すことができます。

  • 大規模なチームがある場合は、ノンブロッキング、関数、宣言的プログラミングへの移行における急な学習曲線に留意してください。完全なスイッチなしで開始する実用的な方法は、リアクティブ WebClient を使用することです。それを超えて、小さく始めて、利益を測定します。幅広いアプリケーションでは、シフトは不要であると予想されます。どのような利点を探すべきかわからない場合は、ノンブロッキング I/O の仕組み(たとえば、シングルスレッド Node.js の同時実行)とその影響について学習することから始めてください。

1.1.5. サーバー

Spring WebFlux は、Tomcat、Jetty、Servlet 3.1+ コンテナー、および Netty や Undertow などの非サーブレットランタイムでサポートされています。すべてのサーバーは低レベルの共通 API に適合しているため、サーバー全体で高レベルのプログラミングモデルをサポートできます。

Spring WebFlux には、サーバーを起動または停止するための組み込みサポートがありません。ただし、Spring 構成と WebFlux インフラストラクチャからアプリケーションをアセンブルし、数行のコードで実行するのは簡単です。

Spring Boot には、これらのステップを自動化する WebFlux スターターがあります。デフォルトでは、スターターは Netty を使用しますが、Maven または Gradle の依存関係を変更することにより、Tomcat、Jetty、または Undertow に簡単に切り替えることができます。Spring Boot のデフォルトは Netty です。これは、非同期のノンブロッキングスペースでより広く使用されており、クライアントとサーバーがリソースを共有できるためです。

Tomcat と Jetty は、Spring MVC と WebFlux の両方で使用できます。ただし、使用方法は大きく異なることに注意してください。Spring MVC はサーブレットブロッキング I/O に依存しており、必要に応じてアプリケーションがサーブレット API を直接使用できるようにします。Spring WebFlux は、Servlet 3.1 ノンブロッキング I/O に依存し、低レベルアダプターの背後でサーブレット API を使用します。直接使用するために公開されていません。

Undertow の場合、Spring WebFlux は、Servlet API なしで Undertow API を直接使用します。

1.1.6. パフォーマンス

パフォーマンスには多くの特性と意味があります。通常、リアクティブでノンブロッキングでは、アプリケーションの実行速度は向上しません。場合によっては(たとえば、WebClient を使用してリモート呼び出しを並列に実行する場合)全体として、ノンブロッキングの方法を実行するにはより多くの作業が必要であり、必要な処理時間をわずかに増加させる可能性があります。

リアクティブでノンブロッキングの主な期待される利点は、少数の固定スレッドと少ないメモリで拡張できることです。予測可能な方法でスケーリングされるため、負荷がかかった場合でもアプリケーションの回復力が高まります。ただし、これらの利点を観察するには、ある程度の遅延(低速で予測不可能なネットワーク I/O の混在を含む)が必要です。それが、リアクティブスタックの強みを示し始めた場所であり、違いは劇的です。

1.1.7. 同時実行モデル

Spring MVC と Spring WebFlux は両方ともアノテーション付きコントローラーをサポートしますが、並行性モデルと、ブロッキングとスレッドのデフォルトの仮定に重要な違いがあります。

Spring MVC(およびサーブレットアプリケーション全般)では、アプリケーションが現在のスレッドをブロックできると想定されています(たとえば、リモート呼び出しの場合)。このため、サーブレットコンテナーは大きなスレッドプールを使用して、リクエスト処理中の潜在的なブロッキングを吸収します。

Spring WebFlux(および一般にノンブロッキングサーバー)では、アプリケーションはブロックされないと想定されています。ノンブロッキングサーバーは、小さな固定サイズのスレッドプール(イベントループワーカー)を使用してリクエストを処理します。

「スケーリングする」と「少ないスレッド」は矛盾しているように聞こえるかもしれませんが、現在のスレッドをブロックしない(および代わりにコールバックに依存する)ことは、吸収するブロッキング呼び出しがないため、余分なスレッドが必要ないことを意味します。
ブロッキング API の呼び出し

ブロッキングライブラリを使用する必要がある場合はどうなるでしょうか? Reactor と RxJava は両方とも、異なるスレッドで処理を続行するために publishOn オペレーターを提供します。つまり、簡単な脱出ハッチがあります。ただし、ブロッキング API はこの同時実行モデルには適していません。

可変状態

Reactor および RxJava では、演算子を使用してロジックを宣言します。実行時に、反応的なパイプラインが形成され、データが異なるステージで順次処理されます。これの主な利点は、パイプライン内のアプリケーションコードが同時に呼び出されることがないため、アプリケーションが変更可能な状態を保護する必要がないことです。

スレッドモデル

Spring WebFlux で実行されているサーバーでどのスレッドを表示する必要がありますか?

  • 「バニラ」Spring WebFlux サーバー(たとえば、データアクセスやその他のオプションの依存関係なし)では、サーバー用に 1 つのスレッドと、リクエスト処理用にいくつかの他のスレッド(通常は CPU コアの数)を期待できます。ただし、サーブレット(ブロッキング)I/O とサーブレット 3.1(ノンブロッキング)I/O の両方の使用をサポートするサーブレットコンテナーは、より多くのスレッド(たとえば、Tomcat で 10)で開始する場合があります。

  • リアクティブ WebClient は、イベントループスタイルで動作します。それに関連する少数の固定数の処理スレッド(たとえば、Reactor Netty コネクターを備えた reactor-http-nio-)を確認できます。ただし、Reactor Netty がクライアントとサーバーの両方に使用される場合、2 つはデフォルトでイベントループリソースを共有します。

  • Reactor および RxJava は、スケジューラと呼ばれるスレッドプールの抽象化を提供し、処理を別のスレッドプールに切り替えるために使用される publishOn 演算子と一緒に使用します。スケジューラーには、特定の並行性戦略を提案する名前があります。たとえば、「並列」(限られた数のスレッドを使用する CPU バウンド作業の場合)または「エラスティック」(多数のスレッドを使用する I/O バウンド作業の場合)このようなスレッドが表示される場合は、一部のコードが特定のスレッドプール Scheduler 戦略を使用していることを意味します。

  • データアクセスライブラリおよびその他のサードパーティの依存関係も、独自のスレッドを作成して使用できます。

設定

Spring Framework は、サーバーの起動と停止のサポートを提供しません。サーバーのスレッドモデルを構成するには、サーバー固有の構成 API を使用するか、Spring Boot を使用する場合は、各サーバーの Spring Boot 構成オプションを確認する必要があります。WebClient を直接構成できます。他のすべてのライブラリについては、それぞれのドキュメントを参照してください。

1.2. リアクティブコア

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

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

    • HttpHandler : ノンブロッキング I/O および Reactive Streams バックプレッシャーを使用した HTTP リクエスト処理の基本契約、および Reactor Netty、Undertow、Tomcat、Jetty、および Servlet 3.1+ コンテナー用のアダプター。

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

  • クライアント側には、ノンブロッキング I/O および Reactive Streams バックプレッシャーで HTTP リクエストを実行するための基本的な ClientHttpConnector 契約と、Reactor Netty (GitHub) およびリアクティブ Jetty HttpClient (GitHub) のアダプターがあります。アプリケーションで使用される上位レベルの WebClient は、この基本契約に基づいています。

  • クライアントおよびサーバーの場合、HTTP リクエストおよびレスポンスコンテンツのシリアライズおよびデシリアライズ用のコーデック

1.2.1. HttpHandler

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

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

サーバー名 使用されるサーバー APIReactive Streams サポート

Netty

Netty API

Reactor Netty (GitHub)

Undertow

Undertow API

spring-web: Reactive Streams ブリッジへの引き綱

Tomcat

Servlet 3.1 ノンブロッキング I/O。ByteBuffers と byte [] を読み書きする Tomcat API

spring-web: Servlet 3.1 ノンブロッキング I/O から Reactive Streams ブリッジ

Jetty

Servlet 3.1 ノンブロッキング I/O。ByteBuffers vs byte [] を書き込む Jetty API

spring-web: Servlet 3.1 ノンブロッキング I/O から Reactive Streams ブリッジ

Servlet 3.1 コンテナー

Servlet 3.1 ノンブロッキング I/O

spring-web: Servlet 3.1 ノンブロッキング I/O から Reactive Streams ブリッジ

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

サーバー名 グループ Id 成果物の名前

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();
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+ コンテナー

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

1.2.2. WebHandler API

org.springframework.web.server パッケージは HttpHandler 契約に基づいており、複数の WebExceptionHandler(Javadoc) 、複数の WebFilter(Javadoc) 、および単一の WebHandler(Javadoc) コンポーネントのチェーンを介してリクエストを処理するための汎用 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

転送されたタイプヘッダーを処理するには、抽出して削除するか、削除するだけです。デフォルトでは使用されません。

フォームデータ

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

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

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

マルチパートデータ

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

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

DefaultServerWebExchange は、構成された HttpMessageReader<MultiValueMap<String, Part>> を使用して、multipart/form-data コンテンツを MultiValueMap に解析します。現在、Synchronoss NIO マルチパート (GitHub) は、サポートされている唯一のサードパーティライブラリであり、マルチパートリクエストのノンブロッキング解析について知っている唯一のライブラリです。ServerCodecConfigurer Bean を介して有効になります(Web ハンドラー API を参照)。

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

転送されたヘッダー

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

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

ForwardedHeaderTransformer は、転送されたヘッダーに基づいてリクエストのホスト、ポート、およびスキームを変更し、それらのヘッダーを削除するコンポーネントです。forwardedHeaderTransformer という名前の Bean として宣言すると、検出されて使用されます。

転送されたヘッダーには、意図したとおりにプロキシによってヘッダーが追加されたか、悪意のあるクライアントによってヘッダーが追加されたかをアプリケーションが認識できないため、セキュリティに関する考慮事項があります。これが、外部からの信頼できない転送トラフィックを削除するように、信頼の境界にあるプロキシを構成する必要がある理由です。ForwardedHeaderTransformer を removeOnly=true で構成することもできます。この場合、ヘッダーは削除されますが、使用されません。

5.1 では、交換が作成される前に、転送されたヘッダーをより早く処理できるように、ForwardedHeaderFilter は非推奨となり、ForwardedHeaderTransformer に置き換えられました。フィルターが構成されている場合、フィルターのリストから除外され、代わりに ForwardedHeaderTransformer が使用されます。

1.2.3. フィルター

WebHandler API では、WebFilter を使用して、フィルターの残りの処理チェーンおよびターゲット WebHandler の前後にインターセプションスタイルのロジックを適用できます。WebFlux 構成を使用する場合、WebFilter の登録は、Spring Bean として宣言し、(オプションで)Bean 宣言で @Order を使用するか、Ordered を実装することによって優先順位を表すのと同じくらい簡単です。

CORS

Spring WebFlux は、コントローラーのアノテーションを通じて CORS 構成のきめ細かなサポートを提供します。ただし、Spring Security で使用する場合は、Spring Security のチェーンフィルターよりも先に並べ替える必要があるビルトイン CorsFilter を使用することをお勧めします。

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

1.2.4. 例外

WebHandler API では、WebExceptionHandler を使用して、WebFilter インスタンスのチェーンおよびターゲット WebHandler からの例外を処理できます。WebFlux 構成を使用する場合、WebExceptionHandler の登録は、Spring Bean として宣言し、(オプションで)Bean 宣言で @Order を使用するか、Ordered を実装することによって優先順位を表すのと同じくらい簡単です。

次の表は、利用可能な WebExceptionHandler 実装を説明しています。

例外ハンドラー 説明

ResponseStatusExceptionHandler

例外の HTTP ステータスコードへのレスポンスを設定することにより、タイプ ResponseStatusException(Javadoc) の例外の処理を提供します。

WebFluxResponseStatusExceptionHandler

@ResponseStatus アノテーションの HTTP ステータスコードも例外で決定できる ResponseStatusExceptionHandler の拡張。

このハンドラーは WebFlux 構成で宣言されています。

1.2.5. コーデック

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

  • Encoder(Javadoc) および Decoder(Javadoc) は、HTTP に依存しないコンテンツをエンコードおよびデコードするための低レベルの契約です。

  • HttpMessageReader(Javadoc) および HttpMessageWriter(Javadoc) は、HTTP メッセージコンテンツをエンコードおよびデコードする契約です。

  • Encoder は EncoderHttpMessageWriter でラップして Web アプリケーションでの使用に適合させることができ、Decoder は DecoderHttpMessageReader でラップできます。

  • DataBuffer(Javadoc) は、さまざまなバイトバッファ表現(たとえば、Netty ByteBufjava.nio.ByteBuffer など)を抽象化し、すべてのコーデックが動作するものです。このトピックの詳細については、「Spring コア」セクションのデータバッファとコーデックを参照してください。

spring-core モジュールは、byte[]ByteBufferDataBufferResource および String エンコーダーとデコーダーの実装を提供します。spring-web モジュールは、Jackson JSON、Jackson Smile、JAXB2、プロトコルバッファー、およびその他のエンコーダーとデコーダーに加えて、フォームデータ、マルチパートコンテンツ、サーバー送信イベントなどの Web 専用 HTTP メッセージリーダーおよびライターの実装を提供します。

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

Jackson JSON

JSON とバイナリ JSON(Smile (GitHub) )は両方とも、Jackson ライブラリが存在する場合にサポートされます。

Jackson2Decoder は次のように機能します。

  • Jackson の非同期のノンブロッキングパーサーは、バイトチャンクのストリームを、それぞれ JSON オブジェクトを表す TokenBuffer に集約するために使用されます。

  • 各 TokenBuffer は Jackson の ObjectMapper に渡され、より高いレベルのオブジェクトを作成します。

  • 単一値のパブリッシャー(例: Mono)にデコードする場合、1 つの TokenBuffer があります。

  • 多値パブリッシャー(Flux など)にデコードする場合、完全に形成されたオブジェクトに対して十分なバイト数が受信されるとすぐに、各 TokenBuffer が ObjectMapper に渡されます。入力コンテンツは、JSON 配列にすることも、コンテンツタイプが application/stream+json の場合は行区切りの JSON (英語) にすることもできます。

Jackson2Encoder は次のように機能します。

  • 単一の値のパブリッシャー(例: Mono)の場合、単に ObjectMapper を介して直列化します。

  • application/json を使用する複数値パブリッシャーの場合、デフォルトでは Flux#collectToList() を使用して値を収集し、結果のコレクションを直列化します。

  • application/stream+json や application/stream+x-jackson-smile などのストリーミングメディアタイプを持つ複数値パブリッシャーの場合、行区切りの JSON (英語) 形式を使用して各値を個別にエンコード、書き込み、フラッシュします。

  • SSE の場合、Jackson2Encoder はイベントごとに呼び出され、出力はフラッシュされ、遅延なく配信されます。

デフォルトでは、Jackson2Encoder と Jackson2Decoder は両方ともタイプ String の要素をサポートしていません。代わりに、デフォルトの仮定は、CharSequenceEncoder によってレンダリングされる直列化された JSON コンテンツを表すストリングまたはストリングのシーケンスです。Flux<String> から JSON 配列をレンダリングする必要がある場合は、Flux#collectToList() を使用して Mono<List<String>> をエンコードします。

フォームデータ

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

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

getFormData() を使用すると、元の生のコンテンツをリクエスト本文から読み取ることはできなくなります。このため、キャッシュされたフォームデータへのアクセスと生のリクエスト本文からの読み取りでは、アプリケーションは ServerWebExchange を一貫して通過することが期待されます。

マルチパート

MultipartHttpMessageReader および MultipartHttpMessageWriter は、「multipart/form-data」コンテンツのデコードおよびエンコードをサポートしています。MultipartHttpMessageReader は、実際の解析のために Flux<Part> に別の HttpMessageReader に委譲し、単純にパーツを収集して MultiValueMap にします。現在、Synchronoss NIO マルチパート (GitHub) は実際の解析に使用されています。

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

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

制限

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

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

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

ストリーミング

HTTP レスポンス(text/event-streamapplication/stream+json など)にストリーミングする場合、切断されたクライアントを後よりも早く確実に検出するために、定期的にデータを送信することが重要です。このような送信は、コメントのみの空の SSE イベント、またはハートビートとして効果的に機能する他の「no-op」データです。

DataBuffer

DataBuffer は、WebFlux のバイトバッファの表現です。このリファレンスの Spring コアの部分については、データバッファとコーデックのセクションで詳しく説明しています。理解しておくべき重要な点は、Netty のような一部のサーバーでは、バイトバッファーがプールされ、参照がカウントされ、メモリリークを回避するために消費されたときに解放する必要があるということです。

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

1.2.6. ロギング

Spring での DEBUG レベルのロギング WebFlux は、コンパクト、最小限、人に優しいように設計されています。これは、特定の課題をデバッグする場合にのみ役立つ他の情報と比較して、何度も役立つ情報の価値の高いビットに焦点を当てています。

TRACE レベルのログは、一般に DEBUG と同じ原則に従います(たとえば、ファイアホースであってはなりません)。ただし、課題のデバッグに使用できます。さらに、一部のログメッセージでは、TRACE と DEBUG で詳細レベルが異なる場合があります。

適切なログは、ログを使用した経験から得られます。記載されているゴールを達成できないものを見つけた場合は、お知らせください。

ログ ID

WebFlux では、単一のリクエストを複数のスレッドで実行できます。スレッド ID は、特定のリクエストに属するログメッセージの関連付けには役立ちません。これがデフォルトで、WebFlux ログメッセージの先頭にリクエスト固有の ID が付けられる理由です。

サーバー側では、ログ ID は ServerWebExchange 属性(LOG_ID_ATTRIBUTE(Javadoc) )に格納されますが、その ID に基づいて完全にフォーマットされたプレフィックスは ServerWebExchange#getLogPrefix() から利用できます。WebClient 側では、ログ ID は ClientRequest 属性(LOG_ID_ATTRIBUTE(Javadoc) )に保存されますが、完全にフォーマットされたプレフィックスは 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()
カスタムコーデック

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

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

次の例は、クライアント側のリクエストに対してこれを行う方法を示しています。

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

1.3. DispatcherHandler

Spring WebFlux は、Spring MVC と同様に、フロントコントローラーパターンを中心に設計されており、中央の WebHandler である DispatcherHandler がリクエスト処理の共有アルゴリズムを提供し、実際の作業は構成可能なデリゲートコンポーネントによって実行されます。このモデルは柔軟性があり、多様なワークフローをサポートします。

DispatcherHandler は、Spring 構成から必要なデリゲートコンポーネントを検出します。また、Spring Bean 自体になるように設計されており、実行されるコンテキストにアクセスするために ApplicationContextAware を実装しています。DispatcherHandler が webHandler という Bean 名で宣言されている場合、WebHandler API に従って、リクエスト処理チェーンをまとめる WebHttpHandlerBuilder(Javadoc) によって検出されます。

WebFlux アプリケーションの Spring 構成には通常、次のものが含まれます。

次の例が示すように、処理はチェーンを構築するために WebHttpHandlerBuilder に与えられます。

Java
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
Kotlin
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()

結果の HttpHandler は、サーバーアダプターで使用する準備ができています。

1.3.1. 特別な Bean タイプ

DispatcherHandler は、リクエストを処理して適切なレスポンスをレンダリングするために特別な Bean に委譲します。「特別な Bean」とは、WebFlux フレームワーク契約を実装する Spring 管理の Object インスタンスを意味します。通常、これらにはビルトイン契約が付属していますが、プロパティをカスタマイズしたり、拡張したり、置き換えたりすることができます。

次の表に、DispatcherHandler によって検出された特別な Bean を示します。下位レベルで検出された他の Bean もいくつかあることに注意してください(Web ハンドラー API の特別な Bean タイプを参照)。

Bean タイプ 説明

HandlerMapping

リクエストをハンドラーにマップします。マッピングはいくつかの条件に基づいており、その詳細は HandlerMapping の実装によって異なります。アノテーション付きコントローラー、単純な URL パターンマッピングなど。

主な HandlerMapping 実装は、@RequestMapping アノテーション付きメソッドの RequestMappingHandlerMapping、関数エンドポイントルートの RouterFunctionMapping、および URI パスパターンと WebHandler インスタンスの明示的な登録の SimpleUrlHandlerMapping です。

HandlerAdapter

ハンドラーが実際に呼び出される方法に関係なく、DispatcherHandler がリクエストにマップされたハンドラーを呼び出すのに役立ちます。例:アノテーション付きコントローラーを呼び出すには、アノテーションを解決する必要があります。HandlerAdapter の主な目的は、そのような詳細から DispatcherHandler を保護することです。

HandlerResultHandler

ハンドラー呼び出しからの結果を処理し、レスポンスを確定します。結果処理を参照してください。

1.3.2. WebFlux 構成

アプリケーションは、リクエストの処理に必要なインフラストラクチャ Bean(Web ハンドラー API および DispatcherHandler にリストされている)を宣言できます。ただし、ほとんどの場合、WebFlux 構成が最適な出発点です。必要な Bean を宣言し、それをカスタマイズするための高レベルの構成コールバック API を提供します。

Spring Boot は WebFlux 構成に依存して Spring WebFlux を構成し、さらに多くの便利なオプションを提供します。

1.3.3. 処理

DispatcherHandler は、リクエストを次のように処理します。

  • 各 HandlerMapping は、一致するハンドラーを見つけるように求められ、最初の一致が使用されます。

  • ハンドラーが見つかると、適切な HandlerAdapter を介して実行され、実行からの戻り値を HandlerResult として公開します。

  • HandlerResult は適切な HandlerResultHandler に渡され、レスポンスに直接書き込むか、ビューを使用してレンダリングすることで処理を完了します。

1.3.4. 結果処理

HandlerAdapter を介したハンドラーの呼び出しからの戻り値は、追加のコンテキストとともに HandlerResult としてラップされ、そのサポートを要求する最初の HandlerResultHandler に渡されます。次の表に、利用可能な HandlerResultHandler 実装を示します。これらはすべて WebFlux 構成で宣言されています。

結果ハンドラーのタイプ 戻り値 デフォルトの順序

ResponseEntityResultHandler

ResponseEntity、通常は @Controller インスタンスから。

0

ServerResponseResultHandler

ServerResponse、通常は関数エンドポイントから。

0

ResponseBodyResultHandler

@ResponseBody メソッドまたは @RestController クラスからの戻り値を処理します。

100

ViewResolutionResultHandler

CharSequenceView(Javadoc) モデル(Javadoc) Mapレンダリング(Javadoc) 、またはその他の Object は、モデル属性として扱われます。

ビューリゾルバーも参照してください。

Integer.MAX_VALUE

1.3.5. 例外

HandlerAdapter から返された HandlerResult は、ハンドラー固有のメカニズムに基づいてエラー処理用の関数を公開できます。このエラー関数は次の場合に呼び出されます:

  • ハンドラー(たとえば、@Controller)呼び出しは失敗します。

  • HandlerResultHandler を介したハンドラー戻り値の処理は失敗します。

エラー関数は、ハンドラーから返されるリアクティブ型がデータ項目を生成する前にエラー信号が発生する限り、レスポンスを(たとえば、エラー状況に)変更できます。

これにより、@Controller クラスの @ExceptionHandler メソッドがサポートされます。対照的に、Spring MVC での同じサポートは HandlerExceptionResolver に基づいています。通常、これは重要ではありません。ただし、WebFlux では、@ControllerAdvice を使用して、ハンドラーが選択される前に発生する例外を処理できないことに注意してください。

「アノテーション付きコントローラー」セクションの例外の管理または WebHandler API セクションの例外も参照してください。

1.3.6. ビューリゾルバー

ビューリゾルバーにより、特定のビューテクノロジに縛られることなく、HTML テンプレートとモデルを使用してブラウザにレンダリングできます。Spring WebFlux では、ViewResolver インスタンスを使用してストリング(論理ビュー名を表す)を View インスタンスにマップする専用 HandlerResultHandler を介して、ビューリゾルバーがサポートされています。次に、View を使用してレスポンスをレンダリングします。

取り扱い

ViewResolutionResultHandler に渡される HandlerResult には、ハンドラーからの戻り値と、リクエスト処理中に追加された属性を含むモデルが含まれます。戻り値は次のいずれかとして処理されます。

  • StringCharSequence: 構成された ViewResolver 実装のリストを通じて View に解決される論理ビュー名。

  • void: リクエストパスに基づいて、先頭と末尾のスラッシュを除いたデフォルトのビュー名を選択し、View に解決します。ビュー名が指定されなかった場合(モデル属性が返された場合など)、または非同期の戻り値(たとえば、空の Mono が完了した場合)でも同じことが起こります。

  • レンダリング (Javadoc) : ビュー解決シナリオの API。コード補完を使用して IDE のオプションを調べましょう。

  • ModelMap: リクエストのモデルに追加される追加のモデル属性。

  • 他 : その他の戻り値(BeanUtils#isSimpleProperty(Javadoc) によって決定される単純型を除く)は、モデルに追加されるモデル属性として扱われます。ハンドラーメソッド @ModelAttribute アノテーションが存在しない限り、属性名は規約(Javadoc) を使用してクラス名から派生します。

モデルには、非同期のリアクティブタイプを含めることができます(たとえば、Reactor または RxJava から)。レンダリングの前に、AbstractView はそのようなモデル属性を具体的な値に解決し、モデルを更新します。単一値のリアクティブ型は単一の値または値なし(空の場合)に解決され、複数値のリアクティブ型(たとえば Flux<T>)は収集されて List<T> に解決されます。

ビューリゾルバーを構成するには、ViewResolutionResultHandler Bean を Spring 構成に追加するだけです。WebFlux 構成は、ビューリゾルバー専用の構成 API を提供します。

Spring WebFlux と統合されたビューテクノロジーの詳細については、ビューテクノロジーを参照してください。

リダイレクト

ビュー名の特別な redirect: プレフィックスを使用すると、リダイレクトを実行できます。UrlBasedViewResolver (およびサブクラス)は、これをリダイレクトが必要な命令として認識します。ビュー名の残りはリダイレクト URL です。

最終的な効果は、コントローラーが RedirectView または Rendering.redirectTo("abc").build() を返した場合と同じですが、コントローラー自体が論理ビュー名の観点から動作できるようになりました。redirect:/some/resource などのビュー名は現在のアプリケーションに関連していますが、redirect:https://example.com/arbitrary/path などのビュー名は絶対 URL にリダイレクトされます。

コンテンツネゴシエーション

ViewResolutionResultHandler はコンテンツネゴシエーションをサポートしています。リクエストメディアタイプと、選択した各 View でサポートされているメディアタイプを比較します。リクエストされたメディアタイプをサポートする最初の View が使用されます。

JSON や XML などのメディアタイプをサポートするために、Spring WebFlux は HttpMessageWriterView を提供します。これは、HttpMessageWriter を介してレンダリングする特別な View です。通常、これらは WebFlux の設定を介してデフォルトビューとして設定します。リクエストされたメディアタイプに一致する場合、デフォルトビューが常に選択され、使用されます。

1.4. アノテーション付きコントローラー

Spring WebFlux は、アノテーションベースのプログラミングモデルを提供します。@Controller および @RestController コンポーネントは、アノテーションを使用して、リクエストマッピング、リクエスト入力、例外処理などを表現します。アノテーション付きコントローラーには柔軟なメソッドシグネチャーがあり、基本クラスを継承したり、特定のインターフェースを実装したりする必要はありません。

次のリストは、基本的な例を示しています。

Java
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}
Kotlin
@RestController
class HelloController {

    @GetMapping("/hello")
    fun handle() = "Hello WebFlux"
}

上記の例では、メソッドはレスポンス本体に書き込まれる String を返します。

1.4.1. @Controller

標準の Spring Bean 定義を使用して、コントローラー Bean を定義できます。@Controller ステレオタイプは自動検出を可能にし、クラスパス内の @Component クラスを検出し、それらの Bean 定義を自動登録するための Spring 一般サポートと連携しています。また、アノテーション付きクラスのステレオタイプとしても機能し、Web コンポーネントとしてのロールを示します。

そのような @Controller Bean の自動検出を有効にするには、次の例に示すように、Java 構成にコンポーネントスキャンを追加できます。

Java
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {

    // ...
}
1org.example.web パッケージをスキャンします。
Kotlin
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {

    // ...
}
1org.example.web パッケージをスキャンします。

@RestController は、それ自体が @Controller および @ResponseBody でメタアノテーションされた合成アノテーションであり、すべてのメソッドが型レベルの @ResponseBody アノテーションを継承するコントローラーを示します。HTML テンプレートを使用して、ビューリゾルバーとレンダリングに対してレスポンス本文に直接書き込みます。

1.4.2. リクエストマッピング

@RequestMapping アノテーションは、リクエストをコントローラーメソッドにマップするために使用されます。URL、HTTP メソッド、リクエストパラメーター、ヘッダー、およびメディアタイプで一致するさまざまな属性があります。クラスレベルで使用して共有マッピングを表現したり、メソッドレベルで使用して特定のエンドポイントマッピングに絞り込んだりできます。

@RequestMapping の HTTP メソッド固有のショートカットバリアントもあります。

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

ほとんどのコントローラーメソッドは、デフォルトですべての HTTP メソッドに一致する @RequestMapping を使用するのではなく、特定の HTTP メソッドにマップされる必要があるため、上記のアノテーションはカスタムアノテーションです。同時に、共有マッピングを表現するには、クラスレベルで @RequestMapping が必要です。

次の例では、タイプおよびメソッドレベルのマッピングを使用しています。

Java
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    fun getPerson(@PathVariable id: Long): Person {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}
URI パターン

glob パターンとワイルドカードを使用してリクエストをマッピングできます:

パターン 説明 サンプル

?

1 文字に一致

"/pages/t?st.html" は "/pages/test.html" および "/pages/t3st.html" と一致する

*

パスセグメント内の 0 個以上の文字に一致する

"/resources/*.png" は "/resources/file.png" と一致する

"/projects/*/versions" は "/projects/spring/versions" と一致しますが、"/projects/spring/boot/versions" とは一致しません

**

パスの終わりまで 0 個以上のパスセグメントに一致する

"/resources/**" は "/resources/file.png" および "/resources/images/file.png" と一致する

** はパスの最後でのみ許可されているため、"/resources/**/file.png" は無効です。

{name}

パスセグメントを照合し、「名前」という名前の変数としてキャプチャーする

"/projects/{project}/versions" は "/projects/spring/versions" と一致し、project=spring をキャプチャーする

{name:[a-z]+}

正規表現 "[a-z]+" を「name」という名前のパス変数として一致させます

"/projects/{project:[a-z]+}/versions" は "/projects/spring/versions" と一致しますが、"/projects/spring1/versions" とは一致しません

{*path}

パスの最後まで 0 個以上のパスセグメントに一致し、「パス」という名前の変数としてキャプチャーする

"/resources/{*file}" は "/resources/images/file.png" と一致し、file=images/file.png をキャプチャーする

次の例に示すように、キャプチャーされた URI 変数には @PathVariable を使用してアクセスできます。

Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
    // ...
}

次の例に示すように、クラスおよびメソッドレベルで URI 変数を宣言できます。

Java
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
1 クラスレベルの URI マッピング。
2 メソッドレベルの URI マッピング。
Kotlin
@Controller
@RequestMapping("/owners/{ownerId}") (1)
class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
        // ...
    }
}
1 クラスレベルの URI マッピング。
2 メソッドレベルの URI マッピング。

URI 変数は自動的に適切な型に変換されるか、TypeMismatchException が発生します。単純型(intlongDate など)はデフォルトでサポートされており、他のデータ型のサポートを登録できます。型変換および DataBinder を参照してください。

URI 変数には明示的に名前を付けることができます(たとえば、@PathVariable("customId"))。ただし、名前が同じで、デバッグ情報または Java 8 の -parameters コンパイラフラグを使用してコードをコンパイルする場合は、詳細を省略できます。

構文 {*varName} は、0 個以上の残りのパスセグメントに一致する URI 変数を宣言します。たとえば、/resources/{*path} は /resources/ のすべてのファイルと一致し、"path" 変数は完全な相対パスをキャプチャーします。

構文 {varName:regex} は、構文 {varName:regex} を持つ正規表現で URI 変数を宣言します。例: /spring-web-3.0.5 .jar の URL を指定すると、次のメソッドは名前、バージョン、およびファイル拡張子を抽出します。

Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
    // ...
}

URI パスパターンには、起動時に PropertyPlaceHolderConfigurer を介してローカル、システム、環境、およびその他のプロパティソースに対して解決される ${…​} プレースホルダーを埋め込むこともできます。これを使用して、たとえば、外部設定に基づいてベース URL をパラメーター化できます。

Spring WebFlux は、URI パス一致サポートのために PathPattern および PathPatternParser を使用します。両方のクラスは spring-web にあり、実行時に多数の URI パスパターンが一致する Web アプリケーションの HTTP URL パスで使用するために特別に設計されています。

Spring WebFlux は、/person などのマッピングが /person.* にも一致する Spring MVC とは異なり、サフィックスパターンマッチングをサポートしていません。URL ベースのコンテンツネゴシエーションでは、必要に応じて、クエリパラメーターを使用することをお勧めします。クエリパラメーターは、より単純で、より明示的で、URL パスベースのエクスプロイトに対して脆弱ではありません。

パターン比較

複数のパターンが URL に一致する場合、比較して最適な一致を見つける必要があります。これは、より具体的なパターンを探す PathPattern.SPECIFICITY_COMPARATOR で行われます。

すべてのパターンについて、URI 変数とワイルドカードの数に基づいてスコアが計算されます。URI 変数のスコアはワイルドカードよりも低くなります。合計スコアの低いパターンが優先されます。2 つのパターンのスコアが同じ場合、長い方が選択されます。

キャッチオールパターン(**{*varName} など)はスコアリングから除外され、代わりに常に最後にソートされます。2 つのパターンが両方とも包括的である場合、長い方が選択されます。

消費可能なメディアタイプ

次の例に示すように、リクエストの Content-Type に基づいてリクエストマッピングを絞り込むことができます。

Java
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    // ...
}
Kotlin
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
    // ...
}

消費属性は否定表現もサポートしています。たとえば、!text/plain は text/plain 以外のコンテンツタイプを意味します。

クラスレベルで共有 consumes 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの consumes 属性は、クラスレベルの宣言を継承するのではなくオーバーライドします。

MediaType は、APPLICATION_JSON_VALUE や APPLICATION_XML_VALUE など、一般的に使用されるメディアタイプに定数を提供します。
生産可能なメディアタイプ

次の例に示すように、Accept リクエストヘッダーとコントローラーメソッドが生成するコンテンツタイプのリストに基づいて、リクエストマッピングを絞り込むことができます。

Java
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
Kotlin
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
    // ...
}

メディアタイプは文字セットを指定できます。否定表現がサポートされています。たとえば、!text/plain は text/plain 以外のコンテンツタイプを意味します。

クラスレベルで共有 produces 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの produces 属性は、クラスレベルの宣言を継承するのではなくオーバーライドします。

MediaType は、一般的に使用されるメディアタイプに定数を提供します。APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
パラメーターとヘッダー

クエリパラメーターの条件に基づいてリクエストマッピングを絞り込むことができます。クエリパラメーターの存在(myParam)、その不在(!myParam)、または特定の値(myParam=myValue)をテストできます。次の例では、値を持つパラメーターをテストします。

Java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1myParam が myValue と等しいことを確認してください。
Kotlin
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
1myParam が myValue と等しいことを確認してください。

次の例に示すように、リクエストヘッダー条件でも同じように使用できます。

Java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1myHeader が myValue と等しいことを確認してください。
Kotlin
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
1myHeader が myValue と等しいことを確認してください。
HTTP HEAD, OPTIONS

@GetMapping および @RequestMapping(method=HttpMethod.GET) は、リクエストマッピングの目的で HTTP HEAD を透過的にサポートします。コントローラーのメソッドを変更する必要はありません。HttpHandler サーバーアダプターに適用されるレスポンスラッパーは、Content-Length ヘッダーが実際にレスポンスに書き込まずに書き込まれたバイト数に設定されるようにします。

デフォルトでは、HTTP OPTIONS は、Allow レスポンスヘッダーを、一致する URL パターンを持つすべての @RequestMapping メソッドにリストされている HTTP メソッドのリストに設定することによって処理されます。

HTTP メソッド宣言のない @RequestMapping の場合、Allow ヘッダーは GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS に設定されます。コントローラーメソッドは、サポートされる HTTP メソッドを常に宣言する必要があります(たとえば、HTTP メソッド固有のバリアント — @GetMapping@PostMapping などを使用して)。

@RequestMapping メソッドを明示的に HTTP HEAD および HTTP OPTIONS にマップできますが、これは一般的なケースでは必要ありません。

カスタムアノテーション

Spring WebFlux は、リクエストのマッピングに合成アノテーションの使用をサポートしています。これらは、それ自体が @RequestMapping でメタアノテーションされ、より狭い、より具体的な目的で @RequestMapping 属性のサブセット(またはすべて)を再宣言するように構成されたアノテーションです。

@GetMapping@PostMapping@PutMapping@DeleteMapping および @PatchMapping は、構成されたアノテーションの例です。ほぼ間違いなく、ほとんどのコントローラーメソッドは、デフォルトですべての HTTP メソッドに一致する @RequestMapping を使用するのではなく、特定の HTTP メソッドにマッピングされる必要があるためです。合成されたアノテーションの例が必要な場合は、それらがどのように宣言されているかを参照してください。

Spring WebFlux は、カスタムリクエストマッチングロジックを持つカスタムリクエストマッピング属性もサポートしています。これは、RequestMappingHandlerMapping のサブクラス化と getCustomMethodCondition メソッドのオーバーライドを必要とする、より高度なオプションです。カスタム属性を確認して、独自の RequestCondition を返すことができます。

明示的な登録

ハンドラーメソッドをプログラムで登録できます。これは、動的な登録や、異なる URL での同じハンドラーの異なるインスタンスなどの高度なケースに使用できます。次の例は、その方法を示しています。

Java
@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }

}
1 ターゲットハンドラーとコントローラーのハンドラーマッピングを挿入します。
2 リクエストマッピングメタデータを準備します。
3 ハンドラーメソッドを取得します。
4 登録を追加します。
Kotlin
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)

        val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)

        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)

        mapping.registerMapping(info, handler, method) (4)
    }
}
1 ターゲットハンドラーとコントローラーのハンドラーマッピングを挿入します。
2 リクエストマッピングメタデータを準備します。
3 ハンドラーメソッドを取得します。
4 登録を追加します。

1.4.3. 処理メソッド

@RequestMapping ハンドラーメソッドには柔軟なシグネチャーがあり、サポートされているコントローラーメソッドの引数と戻り値の範囲から選択できます。

メソッド引数

次の表に、サポートされているコントローラーメソッドの引数を示します。

リアクティブ型(Reactor、RxJava、またはその他)は、ブロッキング I/O(リクエスト本体の読み取りなど)を解決する必要がある引数でサポートされています。これは説明列にマークされています。ブロッキングを必要としない引数では、リアクティブ型は想定されていません。

JDK 1.8 の java.util.Optional は、required 属性(たとえば、@RequestParam@RequestHeader など)を持つアノテーションと組み合わせてメソッド引数としてサポートされ、required=false と同等です。

コントローラーメソッドの引数 説明

ServerWebExchange

完全な ServerWebExchange — HTTP リクエストおよびレスポンス、リクエストおよびセッション属性、checkNotModified メソッドなどのコンテナー。

ServerHttpRequestServerHttpResponse

HTTP リクエストまたはレスポンスへのアクセス。

WebSession

セッションへのアクセス。これは、属性が追加されない限り、新しいセッションの開始を強制しません。リアクティブ型をサポートします。

java.security.Principal

現在認証されているユーザー  —  既知の場合、特定の Principal 実装クラスである可能性があります。リアクティブ型をサポートします。

org.springframework.http.HttpMethod

リクエストの HTTP メソッド。

java.util.Locale

使用可能な最も具体的な LocaleResolver によって決定される現在のリクエストロケール — 実際には、構成された LocaleResolver/LocaleContextResolver

java.util.TimeZone + java.time.ZoneId

LocaleContextResolver によって決定される、現在のリクエストに関連付けられたタイムゾーン。

@PathVariable

URI テンプレート変数へのアクセス用。URI パターンを参照してください。

@MatrixVariable

URI パスセグメントの名前と値のペアへのアクセス用。行列変数を参照してください。

@RequestParam

サーブレットリクエストパラメーターへのアクセス用。パラメーター値は、宣言されたメソッド引数タイプに変換されます。@RequestParam を参照してください。

@RequestParam の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。この表の後の「その他の引数」を参照してください。

@RequestHeader

リクエストヘッダーへのアクセス用。ヘッダー値は、宣言されたメソッド引数タイプに変換されます。@RequestHeader を参照してください。

@CookieValue

クッキーへのアクセス用。Cookie 値は、宣言されたメソッド引数タイプに変換されます。@CookieValue を参照してください。

@RequestBody

HTTP リクエスト本文へのアクセス用。本文コンテンツは、HttpMessageReader インスタンスを使用して、宣言されたメソッド引数タイプに変換されます。リアクティブ型をサポートします。@RequestBody を参照してください。

HttpEntity<B>

リクエストヘッダーと本文へのアクセス用。本体は HttpMessageReader インスタンスで変換されます。リアクティブ型をサポートします。HttpEntity を参照してください。

@RequestPart

multipart/form-data リクエストの一部へのアクセス用。リアクティブ型をサポートします。マルチパートコンテンツおよびマルチパートデータを参照してください。

java.util.Maporg.springframework.ui.Model および org.springframework.ui.ModelMap

HTML コントローラーで使用され、ビューレンダリングの一部としてテンプレートに公開されるモデルへのアクセス用。

@ModelAttribute

データバインディングと検証が適用されたモデル内の既存の属性(存在しない場合はインスタンス化)へのアクセス用。@ModelAttribute および Model および DataBinder を参照してください。

@ModelAttribute の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。この表の後の「その他の引数」を参照してください。

ErrorsBindingResult

コマンドオブジェクト、つまり @ModelAttribute 引数の検証およびデータバインディングからのエラーへのアクセス用。Errors または BindingResult 引数は、検証されたメソッド引数の直後に宣言する必要があります。

SessionStatus + クラスレベル @SessionAttributes

フォーム処理の補完をマークするために、クラスレベルの @SessionAttributes アノテーションを介して宣言されたセッション属性のクリーンアップをトリガーします。詳細については、@SessionAttributes を参照してください。

UriComponentsBuilder

現在のリクエストのホスト、ポート、スキーム、およびパスに関連する URL を準備します。URI リンクを参照してください。

@SessionAttribute

任意のセッション属性へのアクセス — クラスレベルの @SessionAttributes 宣言の結果としてセッションに保存されたモデル属性とは対照的。詳細については、@SessionAttribute を参照してください。

@RequestAttribute

リクエスト属性へのアクセス用。詳細については、@RequestAttribute を参照してください。

その他の引数

メソッドの引数が上記のいずれにも一致しない場合、デフォルトでは、BeanUtils#isSimpleProperty(Javadoc) によって決定される単純型の場合は @RequestParam として、そうでない場合は @ModelAttribute として解決されます。

戻り値

次の表に、サポートされているコントローラーメソッドの戻り値を示します。このよう Reactor、RxJava、などのライブラリからの反応タイプのことを注意または他の一般的にすべての戻り値のためにサポートされています。

コントローラーメソッドの戻り値 説明

@ResponseBody

戻り値は、HttpMessageWriter インスタンスを介してエンコードされ、レスポンスに書き込まれます。@ResponseBody を参照してください。

HttpEntity<B>ResponseEntity<B>

戻り値は、HTTP ヘッダーを含む完全なレスポンスを指定し、本文は HttpMessageWriter インスタンスを介してエンコードされ、レスポンスに書き込まれます。ResponseEntity を参照してください。

HttpHeaders

ヘッダーを含み、本文を含まないレスポンスを返すため。

String

ViewResolver インスタンスで解決され、暗黙的なモデルと一緒に使用されるビュー名 — コマンドオブジェクトと @ModelAttribute メソッドによって決定されます。ハンドラーメソッドは、Model 引数(前述)を宣言することにより、プログラムでモデルを強化することもできます。

View

暗黙的なモデルと一緒にレンダリングするために使用する View インスタンス — コマンドオブジェクトと @ModelAttribute メソッドによって決定されます。ハンドラーメソッドは、Model 引数(前述)を宣言することにより、プログラムでモデルを強化することもできます。

java.util.Maporg.springframework.ui.Model

暗黙的なモデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。

@ModelAttribute

モデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。

@ModelAttribute はオプションです。この表の後の「その他の戻り値」を参照してください。

Rendering

モデルおよびビューのレンダリングシナリオ用の API。

void

void、おそらく非同期(たとえば Mono<Void>)、戻り型(または null 戻り値)を持つメソッドは、ServerHttpResponseServerWebExchange 引数、または @ResponseStatus アノテーションも持っている場合、レスポンスを完全に処理したと見なされます。コントローラーが正の ETag または lastModified タイムスタンプチェックを行った場合も同様です。// TODO:詳細については、コントローラーを参照してください。

上記のいずれにも当てはまらない場合、void 戻り型は、REST コントローラーの「レスポンス本文なし」または HTML コントローラーのデフォルトのビュー名選択を示すこともできます。

Flux<ServerSentEvent>Observable<ServerSentEvent> または他のリアクティブ型

サーバー送信イベントを発行します。ServerSentEvent ラッパーは、データのみを書き込む必要がある場合は省略できます(ただし、produces 属性を使用してマッピングで text/event-stream をリクエストまたは宣言する必要があります)。

その他の戻り値

戻り値が上記のいずれにも一致しない場合、デフォルトでは、ビュー名として、String または void (デフォルトのビュー名の選択が適用される)として、またはモデルに追加されるモデル属性として扱われます。BeanUtils#isSimpleProperty(Javadoc) で決定された単純型でない場合は、未解決のままになります。

型変換

引数が String 以外として宣言されている場合、文字列ベースのリクエスト入力を表す一部のアノテーション付きコントローラーメソッド引数(たとえば、@RequestParam@RequestHeader@PathVariable@MatrixVariable、および @CookieValue)は型変換を必要とする場合があります。

このような場合、構成されたコンバーターに基づいて型変換が自動的に適用されます。デフォルトでは、単純型(intlongDate など)がサポートされています。型変換は、WebDataBinder (DataBinder を参照)または Formatters を FormattingConversionService に登録(Spring フィールドのフォーマットを参照)することによりカスタマイズできます。

行列変数

RFC 3986 (英語) は、パスセグメントの名前と値のペアについて説明します。Spring WebFlux では、Tim Berners-Lee による「古い投稿」 (英語) に基づく「マトリックス変数」と呼ばれますが、URI パスパラメーターとも呼ばれます。

マトリックス変数は任意のパスセグメントに表示でき、各変数はセミコロンで区切られ、複数の値はコンマで区切られます(例: "/cars;color=red,green;year=2012")。"color=red;color=green;color=blue" など、変数名を繰り返し使用して複数の値を指定することもできます。

Spring MVC とは異なり、WebFlux では、URL 内のマトリックス変数の有無はリクエストマッピングに影響しません。つまり、URI コンテンツを使用して変数の内容をマスクする必要はありません。つまり、コントローラーメソッドからマトリックス変数にアクセスする場合は、マトリックス変数が予想されるパスセグメントに URI 変数を追加する必要があります。次の例は、その方法を示しています。

Java
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}
Kotlin
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

    // petId == 42
    // q == 11
}

すべてのパスセグメントにマトリックス変数を含めることができるため、次の例に示すように、マトリックス変数がどのパス変数にあると予想されるかを明確にする必要がある場合があります。

Java
// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

    // q1 == 11
    // q2 == 22
}

次の例に示すように、マトリックス変数をオプションとして定義し、デフォルト値を指定できます。

Java
// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}
Kotlin
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}

すべての行列変数を取得するには、次の例に示すように、MultiValueMap を使用します。

Java
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
Kotlin
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable matrixVars: MultiValueMap<String, String>,
        @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
@RequestParam

@RequestParam アノテーションを使用して、クエリパラメーターをコントローラーのメソッド引数にバインドできます。次のコードスニペットは使用方法を示しています。

Java
@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...
}
1@RequestParam を使用します。
Kotlin
import org.springframework.ui.set

@Controller
@RequestMapping("/pets")
class EditPetForm {

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = clinic.loadPet(petId)
        model["pet"] = pet
        return "petForm"
    }

    // ...
}
1@RequestParam を使用します。
サーブレット API の「リクエストパラメーター」の概念は、クエリパラメーター、フォームデータ、およびマルチパートを 1 つにまとめます。ただし、WebFlux では、それぞれに ServerWebExchange を介して個別にアクセスします。@RequestParam はクエリパラメーターのみにバインドしますが、データバインドを使用して、クエリパラメーター、フォームデータ、およびマルチパートをコマンドオブジェクトに適用できます。

@RequestParam アノテーションを使用するメソッドパラメーターはデフォルトで必須ですが、@RequestParam の必須フラグを false に設定するか、java.util.Optional ラッパーで引数を宣言することにより、メソッドパラメーターがオプションであることを指定できます。

ターゲットメソッドのパラメータータイプが String でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。

@RequestParam アノテーションが Map<String, String> または MultiValueMap<String, String> 引数で宣言されると、マップにはすべてのクエリパラメーターが入力されます。

@RequestParam の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。デフォルトでは、(BeanUtils#isSimpleProperty(Javadoc) によって決定される)単純な値型であり、他の引数リゾルバーによって解決されない引数は、@RequestParam でアノテーションが付けられているかのように扱われます。

@RequestHeader

@RequestHeader アノテーションを使用して、リクエストヘッダーをコントローラーのメソッド引数にバインドできます。

次の例は、ヘッダー付きのリクエストを示しています。

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

次の例は、Accept-Encoding および Keep-Alive ヘッダーの値を取得します。

Java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1Accept-Encoging ヘッダーの値を取得します。
2Keep-Alive ヘッダーの値を取得します。
Kotlin
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
1Accept-Encoging ヘッダーの値を取得します。
2Keep-Alive ヘッダーの値を取得します。

ターゲットメソッドのパラメータータイプが String でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。

@RequestHeader アノテーションが Map<String, String>MultiValueMap<String, String> または HttpHeaders 引数で使用される場合、マップにはすべてのヘッダー値が入力されます。

コンマ区切りの文字列を、文字列の配列またはコレクション、または型変換システムに既知の他の型に変換するための組み込みサポートが利用可能です。例: @RequestHeader("Accept") アノテーションが付けられたメソッドパラメーターは、タイプ String である場合がありますが、String[] または List<String> である場合もあります。
@CookieValue

@CookieValue アノテーションを使用して、HTTP Cookie の値をコントローラーのメソッド引数にバインドできます。

次の例は、Cookie を使用したリクエストを示しています。

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

次のコードサンプルは、Cookie 値を取得する方法を示しています。

Java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
1Cookie 値を取得します。
Kotlin
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
1Cookie 値を取得します。

ターゲットメソッドのパラメータータイプが String でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。

@ModelAttribute

メソッドの引数で @ModelAttribute アノテーションを使用して、モデルの属性にアクセスしたり、存在しない場合はインスタンス化したりできます。また、モデル属性は、フィールド名に一致する名前を持つクエリパラメーターとフォームフィールドの値でオーバーレイされます。これはデータバインディングと呼ばれ、個々のクエリパラメーターとフォームフィールドの解析と変換に対処する必要がなくなります。次の例では、Pet のインスタンスをバインドします。

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1Pet のインスタンスをバインドします。
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1Pet のインスタンスをバインドします。

上記の例の Pet インスタンスは、次のように解決されます。

  • Model を介して既に追加されている場合はモデルから。

  • HTTP セッションから @SessionAttributes を介して。

  • デフォルトのコンストラクターの呼び出しから。

  • クエリパラメーターまたはフォームフィールドに一致する引数を持つ「プライマリコンストラクター」の呼び出しから。引数名は、JavaBeans @ConstructorProperties またはバイトコード内の実行時保持パラメーター名によって決定されます。

モデル属性インスタンスが取得された後、データバインディングが適用されます。WebExchangeDataBinder クラスは、クエリパラメーターとフォームフィールドの名前をターゲット Object のフィールド名に一致させます。一致するフィールドは、必要に応じて型変換が適用された後に入力されます。データバインディング(および検証)の詳細については、検証を参照してください。データバインディングのカスタマイズの詳細については、DataBinder を参照してください。

データバインディングはエラーになる可能性があります。デフォルトでは、WebExchangeBindException が発生しますが、コントローラーメソッドでこのようなエラーをチェックするには、次の例に示すように、@ModelAttribute のすぐ隣に BindingResult 引数を追加できます。

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1BindingResult を追加します。
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
1BindingResult を追加します。

javax.validation.Valid アノテーションまたは Spring の @Validated アノテーションを追加することにより、データバインディング後に検証を自動的に適用できます(Bean バリデーションおよび Spring 検証も参照)。次の例では、@Valid アノテーションを使用しています。

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1 モデル属性引数で @Valid を使用します。
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
1 モデル属性引数で @Valid を使用します。

Spring WebFlux は、Spring MVC とは異なり、モデル内のリアクティブタイプをサポートします。たとえば、Mono<Account> または io.reactivex.Single<Account> です。@ModelAttribute 引数は、リアクティブ型ラッパーの有無にかかわらず宣言できます。必要に応じて、実際の値に応じて解決されます。ただし、BindingResult 引数を使用するには、前述のように、リアクティブ型ラッパーなしで @ModelAttribute 引数を宣言する必要があることに注意してください。または、次の例に示すように、リアクティブ型を使用してエラーを処理できます。

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
    return petMono
            .flatMap { pet ->
                // ...
            }
            .onErrorResume{ ex ->
                // ...
            }
}

@ModelAttribute の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。デフォルトでは、単純な値タイプ(BeanUtils#isSimpleProperty(Javadoc) によって決定される)ではなく、他の引数リゾルバーによって解決されない引数は、@ModelAttribute でアノテーション付けされているものとして扱われます。

@SessionAttributes

@SessionAttributes は、リクエスト間で WebSession にモデル属性を保存するために使用されます。これは、特定のコントローラーが使用するセッション属性を宣言する型レベルのアノテーションです。これは通常、後続のアクセスリクエストのためにセッションに透過的に保存されるモデル属性の名前またはモデル属性のタイプをリストします。

次の例を考えてみましょう。

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1@SessionAttributes アノテーションを使用します。
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
1@SessionAttributes アノテーションを使用します。

最初のリクエストで、名前が pet のモデル属性がモデルに追加されると、自動的に WebSession に昇格して保存されます。次の例に示すように、別のコントローラーメソッドが SessionStatus メソッド引数を使用してストレージをクリアするまで、そこに残ります。

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
        if (errors.hasErrors()) {
            // ...
        }
            status.setComplete();
            // ...
        }
    }
}
1@SessionAttributes アノテーションを使用します。
2SessionStatus 変数を使用します。
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
        if (errors.hasErrors()) {
            // ...
        }
        status.setComplete()
        // ...
    }
}
1@SessionAttributes アノテーションを使用します。
2SessionStatus 変数を使用します。
@SessionAttribute

グローバルに(つまり、フィルターによってコントローラーの外部で)管理される既存のセッション属性にアクセスする必要があり、存在する場合と存在しない場合、メソッドパラメーターで @SessionAttribute アノテーションを使用できます。次の例を示します。

Java
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1@SessionAttribute を使用します。
Kotlin
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}
1@SessionAttribute を使用します。

セッション属性を追加または削除する必要があるユースケースの場合、WebSession をコントローラーメソッドに挿入することを検討してください。

コントローラーワークフローの一部としてセッションでモデル属性を一時的に保存するには、@SessionAttributes に従って、SessionAttributes の使用を検討してください。

@RequestAttribute

@SessionAttribute と同様に、次の例に示すように、@RequestAttribute アノテーションを使用して、以前に作成された既存のリクエスト属性にアクセスできます(たとえば、WebFilter によって)。

Java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
1@RequestAttribute を使用します。
Kotlin
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
1@RequestAttribute を使用します。
マルチパートコンテンツ

マルチパートデータで説明したように、ServerWebExchange はマルチパートコンテンツへのアクセスを提供します。次の例に示すように、コントローラーでファイルアップロードフォーム(ブラウザーなどから)を処理する最良の方法は、コマンドオブジェクトへのデータバインディングを使用することです。

Java
class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        // ...
    }

}
Kotlin
class MyForm(
        val name: String,
        val file: MultipartFile)

@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(form: MyForm, errors: BindingResult): String {
        // ...
    }

}

RESTful サービスシナリオで、ブラウザ以外のクライアントからマルチパートリクエストを送信することもできます。次の例では、JSON とともにファイルを使用しています。

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

次の例に示すように、@RequestPart を使用して個々のパーツにアクセスできます。

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file) { (2)
    // ...
}
1@RequestPart を使用してメタデータを取得します。
2@RequestPart を使用してファイルを取得します。
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file): String { (2)
    // ...
}
1@RequestPart を使用してメタデータを取得します。
2@RequestPart を使用してファイルを取得します。

次の例に示すように、未加工部分のコンテンツをデシリアライズする(たとえば、@RequestBody に似た JSON に)ために、Part の代わりに具体的なターゲット Object を宣言できます。

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
1@RequestPart を使用してメタデータを取得します。
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
    // ...
}
1@RequestPart を使用してメタデータを取得します。

@RequestPart を javax.validation.Valid または Spring の @Validated アノテーションと組み合わせて使用できます。これにより、標準 Bean 検証が適用されます。検証エラーにより WebExchangeBindException が発生し、400(BAD_REQUEST)レスポンスが発生します。例外には、エラーの詳細を含む BindingResult が含まれます。また、非同期ラッパーで引数を宣言し、エラー関連の演算子を使用することで、コントローラーメソッドで処理することもできます。

Java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // use one of the onError* operators...
}
Kotlin
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
    // ...
}

すべてのマルチパートデータに MultiValueMap としてアクセスするには、次の例に示すように、@RequestBody を使用できます。

Java
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
    // ...
}
1@RequestBody を使用します。
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
    // ...
}
1@RequestBody を使用します。

次の例に示すように、ストリーミング形式でマルチパートデータに順次アクセスするには、代わりに @RequestBody と Flux<Part> (または Kotlin の Flow<Part>)を使用できます。

Java
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
    // ...
}
1@RequestBody を使用します。
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
    // ...
}
1@RequestBody を使用します。
@RequestBody

@RequestBody アノテーションを使用して、リクエスト本体を読み取り、HttpMessageReader を介して Object にデシリアライズすることができます。次の例では、@RequestBody 引数を使用しています。

Java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}

Spring MVC とは異なり、WebFlux では、@RequestBody メソッドの引数は、リアクティブ型、完全なノンブロッキング読み取り、および(クライアントからサーバーへの)ストリーミングをサポートします。

Java
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}

WebFlux 構成HTTP メッセージコーデックオプションを使用して、メッセージリーダーを構成またはカスタマイズできます。

@RequestBody を javax.validation.Valid または Spring の @Validated アノテーションと組み合わせて使用できます。これにより、標準 Bean 検証が適用されます。検証エラーにより WebExchangeBindException が発生し、400(BAD_REQUEST)レスポンスが発生します。例外には、エラーの詳細を含む BindingResult が含まれており、非同期ラッパーで引数を宣言し、エラー関連の演算子を使用することにより、コントローラーメソッドで処理できます。

Java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // use one of the onError* operators...
}
Kotlin
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
    // ...
}
HttpEntity

HttpEntity は、@RequestBody を使用する場合とほぼ同じですが、リクエストヘッダーと本文を公開するコンテナーオブジェクトに基づいています。次の例では、HttpEntity を使用しています。

Java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
@ResponseBody

メソッドで @ResponseBody アノテーションを使用して、HttpMessageWriter を介して戻り値をレスポンス本体に直列化できます。次の例は、その方法を示しています。

Java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}

@ResponseBody はクラスレベルでもサポートされます。この場合、すべてのコントローラーメソッドによって継承されます。これは @RestController の効果であり、これは @Controller および @ResponseBody でマークされたメタアノテーションにすぎません。

@ResponseBody はリアクティブ型をサポートします。つまり、Reactor 型または RxJava 型を返し、それらが生成する非同期値をレスポンスにレンダリングさせることができます。詳細については、ストリーミングおよび JSON レンダリングを参照してください。

@ResponseBody メソッドを JSON 直列化ビューと組み合わせることができます。詳細については、Jackson JSON を参照してください。

WebFlux 構成HTTP メッセージコーデックオプションを使用して、メッセージの書き込みを構成またはカスタマイズできます。

ResponseEntity

ResponseEntity は @ResponseBody に似ていますが、ステータスとヘッダーがあります。例:

Java
@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}
Kotlin
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body: String = ...
    val etag: String = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}

WebFlux は、単一値のリアクティブ型を使用して ResponseEntity を非同期的に生成すること、および / またはボディの単一値および複数値のリアクティブ型を使用することをサポートしています。

Jackson JSON

Spring は、Jackson JSON ライブラリのサポートを提供します。

JSON ビュー

Spring WebFlux は、Object のすべてのフィールドのサブセットのみをレンダリングできる Jackson の直列化ビュー (英語) の組み込みサポートを提供します。@ResponseBody または ResponseEntity コントローラーメソッドで使用するには、次の例に示すように、Jackson の @JsonView アノテーションを使用して直列化ビュークラスをアクティブにします。

Java
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}
Kotlin
@RestController
class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView::class)
    fun getUser(): User {
        return User("eric", "7!jd#h23")
    }
}

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String
) {
    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}
@JsonView ではビュークラスの配列を使用できますが、指定できるのはコントローラーメソッドごとに 1 つだけです。複数のビューをアクティブにする必要がある場合は、複合インターフェースを使用します。

1.4.4. Model

@ModelAttribute アノテーションを使用できます。

  • モデルからオブジェクトを作成またはアクセスし、WebDataBinder を介してリクエストにバインドする @RequestMapping メソッドのメソッド引数

  • @Controller または @ControllerAdvice クラスのメソッドレベルのアノテーションとして、@RequestMapping メソッドを呼び出す前にモデルを初期化できます。

  • @RequestMapping メソッドで、戻り値をモデル属性としてマークします。

このセクションでは、@ModelAttribute メソッド、または前述のリストの 2 番目の項目について説明します。コントローラーは、@ModelAttribute メソッドをいくつでも持つことができます。このようなメソッドはすべて、同じコントローラー内の @RequestMapping メソッドの前に呼び出されます。@ModelAttribute メソッドは、@ControllerAdvice を介してコントローラー間で共有することもできます。詳細については、コントローラーのアドバイスのセクションを参照してください。

@ModelAttribute メソッドには、柔軟なメソッドシグネチャーがあります。これらは、@RequestMapping メソッドと同じ引数の多くをサポートします(@ModelAttribute 自体とリクエスト本文に関連するものを除く)。

次の例では、@ModelAttribute メソッドを使用しています。

Java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}

次の例では、1 つの属性のみを追加します。

Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number);
}
名前が明示的に指定されていない場合、Conventions(Javadoc) の javadoc に従って、タイプに基づいてデフォルト名が選択されます。オーバーロードされた addAttribute メソッドを使用するか、@ModelAttribute の name 属性(戻り値用)を使用して、明示的な名前をいつでも割り当てることができます。

Spring WebFlux は、Spring MVC とは異なり、モデル内のリアクティブ型を明示的にサポートします(たとえば、Mono<Account> または io.reactivex.Single<Account>)。次の例に示すように、@ModelAttribute 引数がラッパーなしで宣言されている場合、そのような非同期モデル属性は、@RequestMapping 呼び出しの時点で実際の値に透過的に解決(およびモデル更新)できます。

Java
@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}
Kotlin
import org.springframework.ui.set

@ModelAttribute
fun addAccount(@RequestParam number: String) {
    val accountMono: Mono<Account> = accountRepository.findAccount(number)
    model["account"] = accountMono
}

@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
    // ...
}

さらに、リアクティブ型ラッパーを持つモデル属性は、ビューのレンダリングの直前に実際の値に解決されます(そしてモデルが更新されます)。

@ModelAttribute を @RequestMapping メソッドのメソッドレベルのアノテーションとして使用することもできます。その場合、@RequestMapping メソッドの戻り値はモデル属性として解釈されます。戻り値がビュー名として解釈される String でない限り、これは HTML コントローラーのデフォルトの動作であるため、通常は必要ありません。@ModelAttribute は、次の例に示すように、モデル属性名のカスタマイズにも役立ちます。

Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}

1.4.5. DataBinder

@Controller または @ControllerAdvice クラスは、WebDataBinder のインスタンスを初期化するために、@InitBinder メソッドを持つことができます。これらは次の目的で使用されます。

  • リクエストパラメーター(つまり、フォームデータまたはクエリ)をモデルオブジェクトにバインドします。

  • String ベースのリクエスト値(リクエストパラメーター、パス変数、ヘッダー、Cookie など)をコントローラーメソッド引数のターゲットタイプに変換します。

  • HTML フォームをレンダリングするときに、モデルオブジェクト値を String 値としてフォーマットします。

@InitBinder メソッドは、コントローラー固有の java.bean.PropertyEditor または Spring Converter および Formatter コンポーネントを登録できます。さらに、WebFlux Java 構成を使用して、Converter および Formatter タイプをグローバルに共有される FormattingConversionService に登録できます。

@InitBinder メソッドは、@ModelAttribute (コマンドオブジェクト)引数を除き、@RequestMapping メソッドと同じ引数の多くをサポートします。通常、これらは、登録用の WebDataBinder 引数、および void 戻り値で宣言されます。次の例では、@InitBinder アノテーションを使用しています。

Java
@Controller
public class FormController {

    @InitBinder (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}
1@InitBinder アノテーションを使用します。
Kotlin
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}

または、共有 FormattingConversionService を介して Formatter ベースのセットアップを使用する場合、次の例に示すように、同じアプローチを再利用してコントローラー固有の Formatter インスタンスを登録できます。

Java
@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
    }

    // ...
}
1 カスタムフォーマッタ(この場合は DateFormatter)を追加します。
Kotlin
@Controller
class FormController {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
    }

    // ...
}
1 カスタムフォーマッタ(この場合は DateFormatter)を追加します。

1.4.6. 例外の管理

@Controller および @ControllerAdvice クラスには、コントローラーメソッドからの例外を処理する @ExceptionHandler メソッドを含めることができます。次の例には、このようなハンドラーメソッドが含まれています。

Java
@Controller
public class SimpleController {

    // ...

    @ExceptionHandler (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
1@ExceptionHandler の宣言。
Kotlin
@Controller
class SimpleController {

    // ...

    @ExceptionHandler (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}
1@ExceptionHandler の宣言。

例外は、伝播されるトップレベルの例外(つまり、直接 IOException がスローされる)、またはトップレベルのラッパー例外内の直接の原因(たとえば、IllegalStateException 内にラップされた IOException)に一致します。

例外のタイプを一致させるには、前の例に示すように、ターゲットの例外をメソッド引数として宣言することをお勧めします。または、アノテーション宣言により、一致する例外タイプを絞り込むことができます。通常、引数のシグネチャーをできるだけ具体的に指定し、対応する順序で優先順位付けされた @ControllerAdvice でプライマリルート例外マッピングを宣言することをお勧めします。詳細については、MVC セクションを参照してください。

WebFlux の @ExceptionHandler メソッドは、@RequestMapping メソッドと同じメソッド引数と戻り値をサポートしていますが、リクエスト本体および @ModelAttribute 関連のメソッド引数は例外です。

Spring での @ExceptionHandler メソッドのサポート WebFlux は、@RequestMapping メソッドの HandlerAdapter によって提供されます。詳細については、DispatcherHandler を参照してください。

REST API の例外

REST サービスの一般的な要件は、レスポンスの本文にエラーの詳細を含めることです。Spring Framework は、レスポンス本文のエラー詳細の表現がアプリケーション固有であるため、自動的にそうしません。ただし、@RestController は、ResponseEntity 戻り値を持つ @ExceptionHandler メソッドを使用して、レスポンスのステータスと本文を設定できます。そのようなメソッドを @ControllerAdvice クラスで宣言して、グローバルに適用することもできます。

Spring WebFlux には Spring MVC ResponseEntityExceptionHandler に相当するものがないことに注意してください。これは、WebFlux が ResponseStatusException (またはそのサブクラス)のみを生成し、HTTP ステータスコードに変換する必要がないためです。

1.4.7. コントローラーのアドバイス

通常、@ExceptionHandler@InitBinder および @ModelAttribute メソッドは、それらが宣言されている @Controller クラス(またはクラス階層)内で適用されます。そのようなメソッドを(コントローラー間で)よりグローバルに適用したい場合は、@ControllerAdvice または @RestControllerAdvice アノテーションが付けられたクラスで宣言できます。

@ControllerAdvice には @Component のアノテーションが付いています。これは、そのようなクラスをコンポーネントスキャンによって Spring Bean として登録できることを意味します。@RestControllerAdvice は、@ControllerAdvice と @ResponseBody の両方でアノテーションが付けられた合成アノテーションです。これは、本質的に、@ExceptionHandler メソッドがメッセージ変換(ビューリゾルバーまたはテンプレートのレンダリングに対して)によってレスポンス本文にレンダリングされることを意味します。

起動時に、@RequestMapping および @ExceptionHandler メソッドのインフラストラクチャクラスは、@ControllerAdvice アノテーションが付けられた Spring Bean を検出し、実行時にそれらのメソッドを適用します。グローバル @ExceptionHandler メソッド(@ControllerAdvice から)はローカルメソッド(@Controller から)の後に適用さます。対照的に、グローバル @ModelAttribute および @InitBinder メソッドは、ローカルメソッドの前に適用されます。

デフォルトでは、@ControllerAdvice メソッドはすべてのリクエスト(つまり、すべてのコントローラー)に適用されますが、次の例に示すように、アノテーションの属性を使用してコントローラーのサブセットに絞り込むことができます。

Java
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
Kotlin
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}

前の例のセレクターは実行時に評価され、広範囲に使用するとパフォーマンスに悪影響を与える可能性があります。詳細については、@ControllerAdvice(Javadoc) javadoc を参照してください。

1.5. 関数エンドポイント

Spring WebFlux には、WebFlux.fn が含まれています。WebFlux.fn は、機能を使用してリクエストのルーティングと処理を行い、契約は不変性のために設計されています。これは、アノテーションベースのプログラミングモデルに代わるものですが、それ以外は同じリアクティブコア基盤で実行されます。

1.5.1. 概要

WebFlux.fn では、HTTP リクエストは HandlerFunction で処理されます。ServerRequest を受け取り、遅延された ServerResponse (つまり Mono<ServerResponse>)を返す関数です。リクエストオブジェクトとレスポンスオブジェクトの両方に、HTTP リクエストとレスポンスへの JDK 8 フレンドリーなアクセスを提供する不変の契約があります。HandlerFunction は、アノテーションベースのプログラミングモデルの @RequestMapping メソッドの本体に相当します。

受信リクエストは、RouterFunction を使用してハンドラー関数にルーティングされます: ServerRequest を受け取り、遅延 HandlerFunction (つまり Mono<HandlerFunction>)を返す関数。ルーター関数が一致すると、ハンドラー関数が返されます。それ以外の場合は、空の Mono。RouterFunction は @RequestMapping アノテーションと同等ですが、ルーター関数がデータだけでなく動作も提供するという大きな違いがあります。

RouterFunctions.route() は、次の例に示すように、ルーターの作成を容易にするルータービルダーを提供します。

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();


public class PersonHandler {

    // ...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {
        // ...
    }
}
Kotlin
val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = coRouter { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}


class PersonHandler(private val repository: PersonRepository) {

    // ...

    suspend fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
1Coroutines ルーター DSL を使用してルーターを作成します。Reactive の代替手段も router { } を介して使用できます。

RouterFunction を実行する 1 つの方法は、RouterFunction を HttpHandler に変換し、組み込みサーバーアダプターの 1 つを介してインストールすることです。

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

ほとんどのアプリケーションは、WebFlux Java 構成を介して実行できます。サーバーの実行を参照してください。

1.5.2. HandlerFunction

ServerRequest および ServerResponse は、HTTP リクエストおよびレスポンスへの JDK 8 フレンドリーなアクセスを提供する不変のインターフェースです。リクエストとレスポンスの両方が、ボディストリームに対する Reactive Streams (英語) バックプレッシャーを提供します。リクエスト本体は、Reactor Flux または Mono で表されます。レスポンス本体は、Flux および Mono を含む Reactive Streams Publisher で表されます。詳細については、リアクティブライブラリを参照してください。

ServerRequest

ServerRequest は HTTP メソッド、URI、ヘッダー、およびクエリパラメーターへのアクセスを提供し、ボディへのアクセスは body メソッドを介して提供されます。

次の例では、リクエストの本文を Mono<String> に抽出します。

Java
Mono<String> string = request.bodyToMono(String.class);
Kotlin
val string = request.awaitBody<String>()

次の例では、Flux<Person> (または Kotlin の Flow<Person>)にボディを抽出します。Person オブジェクトは、JSON や XML などの直列化された形式からデコードされます。

Java
Flux<Person> people = request.bodyToFlux(Person.class);
Kotlin
val people = request.bodyToFlow<Person>()

上記の例は、より一般的な ServerRequest.body(BodyExtractor) を使用するショートカットであり、BodyExtractor 機能戦略インターフェースを受け入れます。ユーティリティクラス BodyExtractors は、多数のインスタンスへのアクセスを提供します。例:上記の例は、次のように書くこともできます。

Java
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
Kotlin
    val string = request.body(BodyExtractors.toMono(String::class.java)).awaitFirst()
    val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()

次の例は、フォームデータにアクセスする方法を示しています。

Java
Mono<MultiValueMap<String, String> map = request.formData();
Kotlin
val map = request.awaitFormData()

次の例は、マップとしてマルチパートデータにアクセスする方法を示しています。

Java
Mono<MultiValueMap<String, Part> map = request.multipartData();
Kotlin
val map = request.awaitMultipartData()

次の例は、ストリーミング方式で一度に 1 つずつマルチパートにアクセスする方法を示しています。

Java
Flux<Part> parts = request.body(BodyExtractors.toParts());
Kotlin
val parts = request.body(BodyExtractors.toParts()).asFlow()
ServerResponse

ServerResponse は HTTP レスポンスへのアクセスを提供します。これは不変なので、build メソッドを使用して HTTP レスポンスを作成できます。ビルダーを使用して、レスポンスステータスを設定したり、レスポンスヘッダーを追加したり、本文を提供したりできます。次の例では、JSON コンテンツで 200(OK)レスポンスを作成します。

Java
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
Kotlin
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)

次の例は、Location ヘッダーを持ち、本文がない 201(CREATED)レスポンスを作成する方法を示しています。

Java
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()

使用されるコーデックに応じて、ヒントパラメーターを渡して、本体の直列化または逆直列化の方法をカスタマイズできます。例:Jackson JSON ビュー (英語) を指定するには:

Java
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Kotlin
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
ハンドラークラス

次の例に示すように、ハンドラー関数をラムダとして記述できます。

Java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
Kotlin
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }

これは便利ですが、アプリケーションでは複数の関数が必要であり、複数のインラインラムダが乱雑になる可能性があります。関連するハンドラー関数を、アノテーションベースのアプリケーションの @Controller と同様のロールを持つハンドラークラスにグループ化すると便利です。例:次のクラスは、リアクティブ Person リポジトリを公開します。

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
            .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}
1listPeople は、リポジトリで見つかったすべての Person オブジェクトを JSON として返すハンドラー関数です。
2createPerson は、リクエスト本文に含まれる新しい Person を保存するハンドラー関数です。PersonRepository.savePerson(Person) は Mono<Void> を返すことに注意してください。空の Mono は、リクエストから人が読み取られて保管されたときに完了シグナルを発行します。そのため、build(Publisher<Void>) メソッドを使用して、完了信号を受信したとき(つまり、Person が保存されたとき)にレスポンスを送信します。
3getPerson は、id パス変数で識別される 1 人の人物を返すハンドラー関数です。Person をリポジトリから取得し、見つかった場合は JSON レスポンスを作成します。見つからない場合は、switchIfEmpty(Mono<T>) を使用して 404 未検出レスポンスを返します。
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: Flow<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.awaitBody<Person>()
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
                ?: ServerResponse.notFound().buildAndAwait()

    }
}
1listPeople は、リポジトリで見つかったすべての Person オブジェクトを JSON として返すハンドラー関数です。
2createPerson は、リクエスト本文に含まれる新しい Person を保存するハンドラー関数です。PersonRepository.savePerson(Person) は、戻り値のない一時停止関数であることに注意してください。
3getPerson は、id パス変数で識別される 1 人の人物を返すハンドラー関数です。Person をリポジトリから取得し、見つかった場合は JSON レスポンスを作成します。見つからない場合、404 未検出レスポンスを返します。
検証

関数エンドポイントは、Spring の検証機能を使用して、リクエスト本文に検証を適用できます。例: Person のカスタム Spring バリデーター実装が指定された場合:

Java
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
        return ok().build(repository.savePerson(person));
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
1Validator インスタンスを作成します。
2 検証を適用します。
3400 レスポンスに対して例外を発生させます。
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.awaitBody<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
1Validator インスタンスを作成します。
2 検証を適用します。
3400 レスポンスに対して例外を発生させます。

ハンドラーは、LocalValidatorFactoryBean に基づいてグローバル Validator インスタンスを作成および注入することにより、標準 Bean 検証 API(JSR-303)を使用することもできます。Spring 検証を参照してください。

1.5.3. RouterFunction

ルーター関数を使用して、リクエストを対応する HandlerFunction にルーティングします。通常、ルーター関数は自分で作成するのではなく、RouterFunctions ユーティリティクラスのメソッドを使用して作成します。RouterFunctions.route() (パラメーターなし)は、ルーター関数を作成するための流れるようなビルダーを提供しますが、RouterFunctions.route(RequestPredicate, HandlerFunction) はルーターを直接作成する方法を提供します。

通常、route() ビルダーを使用することをお勧めします。これは、検出が困難な静的インポートを必要とせずに、一般的なマッピングシナリオに便利なショートカットを提供するためです。たとえば、ルーター関数ビルダーは、GET リクエストのマッピングを作成するメソッド GET(String, HandlerFunction) を提供します。POST 用の POST(String, HandlerFunction)

HTTP メソッドベースのマッピングに加えて、ルートビルダーは、リクエストにマッピングするときに追加の述語を導入する方法を提供します。HTTP メソッドごとに、RequestPredicate をパラメーターとして受け取るオーバーロードされたバリアントがありますが、追加の制約を表現できます。

述部

独自の RequestPredicate を作成できますが、RequestPredicates ユーティリティクラスは、リクエストパス、HTTP メソッド、コンテンツタイプなどに基づいて、一般的に使用される実装を提供します。次の例では、リクエスト述語を使用して、Accept ヘッダーに基づいて制約を作成します。

Java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().bodyValue("Hello World")).build();
Kotlin
val route = coRouter {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().bodyValueAndAwait("Hello World")
    }
}

以下を使用して、複数のリクエスト述語を一緒に作成できます。

  • RequestPredicate.and(RequestPredicate) —  両方が一致する必要があります。

  • RequestPredicate.or(RequestPredicate) —  どちらも一致できます。

RequestPredicates の述部の多くが構成されています。例: RequestPredicates.GET(String) は RequestPredicates.method(HttpMethod) と RequestPredicates.path(String) から構成されています。上記の例では、ビルダーが RequestPredicates.GET を内部で使用し、それを accept 述語で構成しているため、2 つのリクエスト述語も使用しています。

ルート

ルーター関数は順番に評価されます。最初のルートが一致しない場合、2 番目のルートが評価され、以下同様に評価されます。一般的なルートの前に、より具体的なルートを宣言することは理にかなっています。この動作は、「最も具体的な」コントローラーメソッドが自動的に選択されるアノテーションベースのプログラミングモデルとは異なることに注意してください。

ルーター関数ビルダーを使用する場合、定義されたすべてのルートは、build() から返される 1 つの RouterFunction に構成されます。複数のルーター関数を一緒に構成する他の方法もあります。

  • RouterFunctions.route() ビルダーの add(RouterFunction) 

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) —  RouterFunctions.route() をネストした RouterFunction.and() のショートカット。

次の例は、4 つのルートの構成を示しています。

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
1 JSON に一致する Accept ヘッダーを持つ GET /person/{id} は PersonHandler.getPerson にルーティングされます
2 JSON に一致する Accept ヘッダーを持つ GET /person は PersonHandler.listPeople にルーティングされます
3 追加の述部のない POST /person は PersonHandler.createPerson にマップされます
4otherRoute は、他の場所で作成され、構築されたルートに追加されるルーター関数です。
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute: RouterFunction<ServerResponse> = coRouter {  }

val route = coRouter {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 JSON に一致する Accept ヘッダーを持つ GET /person/{id} は PersonHandler.getPerson にルーティングされます
2 JSON に一致する Accept ヘッダーを持つ GET /person は PersonHandler.listPeople にルーティングされます
3 追加の述部のない POST /person は PersonHandler.createPerson にマップされます
4otherRoute は、他の場所で作成され、構築されたルートに追加されるルーター関数です。
ネストされたルート

ルーター関数のグループが共有述語(共有パスなど)を持つことは一般的です。上記の例では、共有述語は、3 つのルートで使用される /person に一致するパス述語になります。アノテーションを使用する場合、/person にマップする型レベルの @RequestMapping アノテーションを使用して、この重複を削除します。WebFlux.fn では、経路述部は、ルーター関数ビルダーの path メソッドを介して共有できます。たとえば、上記の例の最後の数行は、ネストされたルートを使用して次のように改善できます。

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET("", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .build();
1path の 2 番目のパラメーターは、ルータービルダーを使用するコンシューマーであることに注意してください。
Kotlin
val route = coRouter {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET("", accept(APPLICATION_JSON), handler::listPeople)
        POST("/person", handler::createPerson)
    }
}

パスベースのネストが最も一般的ですが、Builder で nest メソッドを使用して、あらゆる種類の述語にネストできます。上記には、共有 Accept -header 述部の形式での重複がまだ含まれています。nest メソッドと accept を併用すると、さらに改善できます。

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .build();
Kotlin
val route = coRouter {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
        }
    }
}

1.5.4. サーバーの実行

HTTP サーバーでルーター関数をどのように実行しますか?簡単なオプションは、次のいずれかを使用して、ルーター関数を HttpHandler に変換することです。

  • RouterFunctions.toHttpHandler(RouterFunction)

  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

その後、サーバー固有の手順について HttpHandler に従うことにより、返された HttpHandler を多数のサーバーアダプターで使用できます。

Spring Boot でも使用されるより一般的なオプションは、WebFlux 構成を介して DispatcherHandler ベースのセットアップで実行することです。WebFlux 構成は、Spring 構成を使用して、リクエストの処理に必要なコンポーネントを宣言します。WebFlux Java 構成は、関数エンドポイントをサポートするために次のインフラストラクチャコンポーネントを宣言します。

  • RouterFunctionMapping: Spring 構成内の 1 つ以上の RouterFunction<?> Bean を検出し、RouterFunction.andOther を介して結合し、結果の合成された RouterFunction にリクエストをルーティングします。

  • HandlerFunctionAdapterDispatcherHandler がリクエストにマップされた HandlerFunction を呼び出せるようにする単純なアダプター。

  • ServerResponseResultHandlerServerResponse の writeTo メソッドを呼び出すことにより、HandlerFunction の呼び出しからの結果を処理します。

上記のコンポーネントにより、関数エンドポイントは DispatcherHandler リクエスト処理ライフサイクルに適合し、アノテーション付きコントローラー(存在する場合)と並行して(潜在的に)実行されます。また、Spring Boot WebFlux スターターが関数エンドポイントを有効にする方法でもあります。

次の例は、WebFlux Java 構成を示しています(実行方法については、DispatcherHandler を参照してください)。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}

1.5.5. ハンドラー関数のフィルタリング

ルーティング関数ビルダーで beforeafter または filter メソッドを使用して、ハンドラー関数をフィルター処理できます。アノテーションを使用すると、@ControllerAdviceServletFilter、またはその両方を使用して同様の機能を実現できます。フィルターは、ビルダーによって構築されたすべてのルートに適用されます。これは、ネストされたルートで定義されたフィルターが「トップレベル」ルートに適用されないことを意味します。たとえば、次の例を考えてみましょう。

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
1 カスタムリクエストヘッダーを追加する before フィルターは、2 つの GET ルートにのみ適用されます。
2 レスポンスを記録する after フィルターは、ネストされたものを含むすべてのルートに適用されます。
Kotlin
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
        POST("/person", handler::createPerson)
        after { _, response -> (2)
            logResponse(response)
        }
    }
}
1 カスタムリクエストヘッダーを追加する before フィルターは、2 つの GET ルートにのみ適用されます。
2 レスポンスを記録する after フィルターは、ネストされたものを含むすべてのルートに適用されます。

ルータービルダーの filter メソッドは HandlerFilterFunction を取ります。これは、ServerRequest と HandlerFunction を取り、ServerResponse を返す関数です。ハンドラー関数パラメーターは、チェーンの次の要素を表します。これは通常、ルーティング先のハンドラーですが、複数が適用される場合は別のフィルターにすることもできます。

特定のパスが許可されているかどうかを判断できる SecurityManager があると仮定して、ルートに簡単なセキュリティフィルターを追加できます。次の例は、その方法を示しています。

Java
SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
Kotlin
val securityManager: SecurityManager = ...

val route = router {
        ("/person" and accept(APPLICATION_JSON)).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next(request)
                }
                else {
                    status(UNAUTHORIZED).build();
                }
            }
        }
    }

上記の例は、next.handle(ServerRequest) の呼び出しがオプションであることを示しています。アクセスが許可されている場合にのみハンドラー関数を実行させます。

ルーター関数ビルダーで filter メソッドを使用する以外に、RouterFunction.filter(HandlerFilterFunction) を介して既存のルーター関数にフィルターを適用することができます。

関数エンドポイントの CORS サポートは、専用の CorsWebFilter を介して提供されます。

1.6. URI リンク

このセクションでは、URI を準備するために Spring Framework で使用可能なさまざまなオプションについて説明します。

1.6.1. UriComponents

Spring MVC および Spring WebFlux

UriComponentsBuilder は、次の例に示すように、変数を持つ URI テンプレートから URI を作成できます。

Java
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
1URI テンプレートを使用した静的ファクトリメソッド。
2URI コンポーネントを追加または置換します。
3URI テンプレートと URI 変数をエンコードするようリクエストします。
4UriComponents をビルドします。
5 変数を展開し、URI を取得します。
Kotlin
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build() (4)

val uri = uriComponents.expand("Westin", "123").toUri()  (5)
1URI テンプレートを使用した静的ファクトリメソッド。
2URI コンポーネントを追加または置換します。
3URI テンプレートと URI 変数をエンコードするようリクエストします。
4UriComponents をビルドします。
5 変数を展開し、URI を取得します。

前述の例は、次の例に示すように、1 つのチェーンに統合し、buildAndExpand で短縮できます。

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri()

次の例に示すように、URI に直接移動することで(エンコードを暗示する)、さらに短くすることができます。

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")

次の例に示すように、完全な URI テンプレートを使用してさらに短くします。

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")

1.6.2. UriBuilder

Spring MVC および Spring WebFlux

UriComponentsBuilder は UriBuilder を実装しています。UriBuilderFactory を使用して、UriBuilder を作成できます。UriBuilderFactory と UriBuilder は、ベース URL、エンコード設定、その他の詳細などの共有構成に基づいて、URI テンプレートから URI を構築するプラグ可能なメカニズムを提供します。

RestTemplate および WebClient を UriBuilderFactory で構成して、URI の準備をカスタマイズできます。DefaultUriBuilderFactory は、UriComponentsBuilder を内部で使用し、共有構成オプションを公開する UriBuilderFactory のデフォルト実装です。

次の例は、RestTemplate を構成する方法を示しています。

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory

次の例では、WebClient を構成します。

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val client = WebClient.builder().uriBuilderFactory(factory).build()

さらに、DefaultUriBuilderFactory を直接使用することもできます。UriComponentsBuilder の使用に似ていますが、次の例に示すように、静的ファクトリメソッドの代わりに、構成と設定を保持する実際のインスタンスです。

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")

1.6.3. URI エンコーディング

Spring MVC および Spring WebFlux

UriComponentsBuilder は、2 つのレベルでエンコードオプションを公開します。

どちらのオプションも、非 ASCII 文字と不正な文字をエスケープされたオクテットに置き換えます。ただし、最初のオプションは、URI 変数に表示される予約された意味で文字を置き換えます。

「;」を検討してください。これはパスでは有効ですが、意味は予約されています。最初のオプションは「;」を置き換えます。URI 変数には「%3B」が含まれますが、URI テンプレートには含まれません。対照的に、2 番目のオプションはパス内の正当な文字であるため、「;」を置き換えることはありません。

ほとんどの場合、最初のオプションは URI 変数を完全にエンコードされる不透明なデータとして扱うため、期待される結果が得られる可能性がありますが、オプション 2 は URI 変数に意図的に予約文字が含まれている場合にのみ役立ちます。

次の例では、最初のオプションを使用しています。

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri();

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri()

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"

次の例に示すように、URI に直接移動することで、前述の例を短縮できます(エンコードを意味します)。

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")

次の例に示すように、完全な URI テンプレートを使用してさらに短縮できます。

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")

WebClient および RestTemplate は、UriBuilderFactory 戦略により内部的に URI テンプレートを展開およびエンコードします。どちらもカスタム戦略で構成できます。次の例に示すように:

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()

DefaultUriBuilderFactory 実装は、UriComponentsBuilder を内部的に使用して URI テンプレートを展開およびエンコードします。ファクトリとして、以下のエンコードモードのいずれかに基づいて、エンコードへのアプローチを構成する単一の場所を提供します。

  • TEMPLATE_AND_VALUES: 前のリストの最初のオプションに対応する UriComponentsBuilder#encode() を使用して、URI テンプレートを事前エンコードし、展開時に URI 変数を厳密にエンコードします。

  • VALUES_ONLY: URI テンプレートをエンコードせず、代わりに、UriUtils#encodeUriUriVariables を使用して URI 変数をテンプレートに展開する前に URI 変数に厳密なエンコードを適用します。

  • URI_COMPONENT: 前のリストの 2 番目のオプションに対応する UriComponents#encode() を使用して、URI 変数が展開された後に URI コンポーネント値をエンコードします。

  • NONE: エンコードは適用されません。

RestTemplate は、歴史的な理由と後方互換性のために EncodingMode.URI_COMPONENT に設定されています。WebClient は、DefaultUriBuilderFactory のデフォルト値に依存しています。これは、5.0.x の EncodingMode.URI_COMPONENT から 5.1 の EncodingMode.TEMPLATE_AND_VALUES に変更されました。

1.7. CORS

Spring WebFlux を使用すると、CORS(クロスオリジンリソース共有)を処理できます。このセクションでは、その方法について説明します。

1.7.1. 導入

セキュリティ上の理由から、ブラウザは現在のオリジンの外部のリソースへの AJAX 呼び出しを禁止しています。例:銀行口座を 1 つのタブに、evil.com を別のタブに持つことができます。evil.com のスクリプトは、アカウントからお金を引き出すなど、認証情報を使用して銀行の API に AJAX リクエストを行うことはできません!

Cross-Origin Resource Sharing(CORS)は、ほとんどのブラウザ (英語) で実装されている W3C 仕様 (英語) であり、IFRAME または JSONP に基づく安全性が低く、強力ではない回避策を使用するのではなく、認可される種類のクロスドメインリクエストを指定できます。

1.7.2. 処理

CORS 仕様は、プリフライト、シンプル、および実際のリクエストを区別します。CORS がどのように機能するかを学ぶために、他の多くの人と一緒にこの記事 (英語) を読むか、詳細について仕様を参照してください。

Spring WebFlux HandlerMapping 実装は、CORS の組み込みサポートを提供します。リクエストをハンドラーに正常にマッピングした後、HandlerMapping は指定されたリクエストとハンドラーの CORS 設定をチェックし、さらにアクションを実行します。プリフライトリクエストは直接処理されますが、シンプルで実際の CORS リクエストはインターセプトされ、検証され、必要な CORS レスポンスヘッダーが設定されます。

クロスオリジンリクエストを有効にするには(つまり、Origin ヘッダーが存在し、リクエストのホストとは異なる)、明示的に宣言された CORS 構成が必要です。一致する CORS 設定が見つからない場合、プリフライトリクエストは拒否されます。CORS ヘッダーは単純および実際の CORS リクエストのレスポンスに追加されないため、ブラウザーはそれらを拒否します。

各 HandlerMapping は、URL パターンベースの CorsConfiguration マッピングで個別に構成(Javadoc) できます。ほとんどの場合、アプリケーションは WebFlux Java 構成を使用してこのようなマッピングを宣言します。これにより、単一のグローバルマップがすべての HandlerMapping 実装に渡されます。

HandlerMapping レベルのグローバル CORS 構成と、よりきめ細かいハンドラーレベルの CORS 構成を組み合わせることができます。例:アノテーション付きコントローラーは、クラスまたはメソッドレベルの @CrossOrigin アノテーションを使用できます(他のハンドラーは CorsConfigurationSource を実装できます)。

グローバル設定とローカル設定を結合するためのルールは、一般的に付加的です。たとえば、すべてのグローバルおよびすべてのローカルオリジン。allowCredentials や maxAge など、単一の値しか受け入れられない属性の場合、ローカルはグローバル値をオーバーライドします。詳細については、CorsConfiguration#combine(CorsConfiguration)(Javadoc) を参照してください。

ソースから詳細を確認するか、高度なカスタマイズを行うには、次を参照してください。

  • CorsConfiguration

  • CorsProcessor および DefaultCorsProcessor

  • AbstractHandlerMapping

1.7.3. @CrossOrigin

@CrossOrigin(Javadoc) アノテーションは、次の例に示すように、アノテーション付きコントローラーメソッドでクロスオリジンリクエストを有効にします。

Java
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}

デフォルトでは、@CrossOrigin は次を許可します。

  • すべての起源。

  • すべてのヘッダー。

  • コントローラーメソッドがマップされるすべての HTTP メソッド。

allowedCredentials は、ユーザー固有の機密情報(Cookie や CSRF トークンなど)を公開する信頼レベルを確立し、適切な場合にのみ使用する必要があるため、デフォルトでは有効になっていません。

maxAge は 30 分に設定されています。

@CrossOrigin はクラスレベルでもサポートされ、すべてのメソッドに継承されます。次の例では、特定のドメインを指定し、maxAge を 1 時間に設定します。

Java
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}

次の例に示すように、@CrossOrigin はクラスレベルとメソッドレベルの両方で使用できます。

Java
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
1 クラスレベルで @CrossOrigin を使用します。
2 メソッドレベルで @CrossOrigin を使用します。
Kotlin
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
1 クラスレベルで @CrossOrigin を使用します。
2 メソッドレベルで @CrossOrigin を使用します。

1.7.4. グローバル構成

細分化されたコントローラーメソッドレベルの構成に加えて、おそらくグローバルな CORS 構成も定義する必要があります。URL ベースの CorsConfiguration マッピングは、HandlerMapping で個別に設定できます。ただし、ほとんどのアプリケーションは、WebFlux Java 構成を使用してこれを実行します。

デフォルトでは、グローバル構成により以下が有効になります。

  • すべての起源。

  • すべてのヘッダー。

  • GETHEAD および POST メソッド。

allowedCredentials はデフォルトでは有効ではありません。これは、ユーザー固有の機密情報(Cookie や CSRF トークンなど)を公開する信頼レベルを確立し、適切な場合にのみ使用する必要があるためです。

maxAge は 30 分に設定されています。

WebFlux Java 構成で CORS を有効にするには、次の例に示すように、CorsRegistry コールバックを使用できます。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

        registry.addMapping("/api/**")
                .allowedOrigins("https://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600)

        // Add more mappings...
    }
}

1.7.5. CORS WebFilter

組み込みの CorsWebFilter(Javadoc) を使用して CORS サポートを適用できます。これは、関数エンドポイントに適しています

CorsFilter を Spring Security で使用しようとする場合、Spring Security には CORS のサポートが組み込まれていることに注意してください。

フィルターを構成するには、次の例に示すように、CorsWebFilter Bean を宣言し、CorsConfigurationSource をコンストラクターに渡すことができます。

Java
@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}
Kotlin
@Bean
fun corsFilter(): CorsWebFilter {

    val config = CorsConfiguration()

    // Possibly...
    // config.applyPermitDefaultValues()

    config.allowCredentials = true
    config.addAllowedOrigin("https://domain1.com")
    config.addAllowedHeader("*")
    config.addAllowedMethod("*")

    val source = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration("/**", config)
    }
    return CorsWebFilter(source)
}

1.8. Web セキュリティ

Spring Security プロジェクトは、悪意のある悪用から Web アプリケーションを保護するためのサポートを提供します。以下を含む Spring Security リファレンスドキュメントを参照してください。

1.9. ビューテクノロジー

Spring WebFlux でのビュー技術の使用はプラグ可能です。Thymeleaf、FreeMarker、またはその他の表示技術を使用するかどうかは、主に構成の変更の問題です。この章では、Spring WebFlux と統合されたビューテクノロジーについて説明します。すでにビューリゾルバーに精通していることを前提としています。

1.9.1. Thymeleaf

Thymeleaf は、ダブルクリックでブラウザーでプレビューできる自然な HTML テンプレートを強調する最新のサーバー側 Java テンプレートエンジンです。これは、UI テンプレート(デザイナーなど)での独立した作業に必要なく、非常に役立ちます。実行中のサーバー。Thymeleaf は広範な機能セットを提供し、積極的に開発および保守されています。より完全な導入については、Thymeleaf (英語) プロジェクトのホームページを参照してください。

Thymeleaf と Spring WebFlux の統合は、Thymeleaf プロジェクトによって管理されています。構成には、SpringResourceTemplateResolverSpringWebFluxTemplateEngine や ThymeleafReactiveViewResolver などのいくつかの Bean 宣言が含まれます。詳細については、Thymeleaf+Spring (英語) および WebFlux 統合発表 (英語) を参照してください。

1.9.2. FreeMarker

Apache FreeMarker (英語) は、HTML からメールなどへのあらゆる種類のテキスト出力を生成するためのテンプレートエンジンです。Spring Framework には、Spring WebFlux と FreeMarker テンプレートを使用するための組み込みの統合機能があります。

構成を表示

次の例は、FreeMarker をビューテクノロジーとして設定する方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure FreeMarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates/freemarker")
    }
}

テンプレートは、前の例に示されている FreeMarkerConfigurer で指定されたディレクトリに保存する必要があります。上記の構成で、コントローラーがビュー名 welcome を返す場合、リゾルバーは classpath:/templates/freemarker/welcome.ftl テンプレートを探します。

FreeMarker の設定

FreeMarkerConfigurer Bean で適切な Bean プロパティを設定することにより、FreeMarker の「設定」と「SharedVariables」を FreeMarker Configuration オブジェクト(Spring によって管理される)に直接渡すことができます。freemarkerSettings プロパティには java.util.Properties オブジェクトが必要で、freemarkerVariables プロパティには java.util.Map が必要です。次の例は、FreeMarkerConfigurer の使用方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // ...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("xml_escape", new XmlEscape());

        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        configurer.setFreemarkerVariables(variables);
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // ...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
        setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
    }
}

Configuration オブジェクトに適用される設定と変数の詳細については、FreeMarker のドキュメントを参照してください。

フォーム処理

Spring は、特に <spring:bind/> 要素を含む JSP で使用するタグライブラリを提供します。この要素により、フォームは主にフォームバッキングオブジェクトの値を表示し、Web またはビジネス層の Validator からの検証の失敗の結果を表示できます。Spring は、FreeMarker と同じ機能をサポートしていますが、フォーム入力要素自体を生成するための追加の便利なマクロがあります。

バインドマクロ

マクロの標準セットは、FreeMarker の spring-webflux.jar ファイル内で維持されるため、適切に構成されたアプリケーションで常に使用可能です。

Spring テンプレートライブラリで定義されているマクロの一部は内部(プライベート)と見なされますが、マクロ定義にはそのようなスコープは存在せず、呼び出し元のコードとユーザーテンプレートにすべてのマクロが表示されます。次のセクションでは、テンプレート内から直接呼び出す必要があるマクロのみに焦点を当てます。マクロコードを直接表示する場合、ファイルは spring.ftl と呼ばれ、org.springframework.web.reactive.result.view.freemarker パッケージに含まれています。

バインディングサポートの詳細については、Spring MVC の簡単なバインドを参照してください。

フォームマクロ

FreeMarker テンプレートに対する Spring のフォームマクロサポートの詳細については、Spring MVC ドキュメントの次のセクションを参照してください。

1.9.3. スクリプトビュー

Spring Framework には、JSR-223 (英語) Java スクリプトエンジン上で実行できるテンプレートライブラリで Spring WebFlux を使用するための組み込みの統合があります。次の表は、さまざまなスクリプトエンジンでテストしたテンプレートライブラリを示しています。

スクリプトライブラリ スクリプトエンジン

Handlebars (英語)

Nashorn (英語)

Mustache (英語)

Nashorn (英語)

React (英語)

Nashorn (英語)

EJS (英語)

Nashorn (英語)

ERB (英語)

JRuby (英語)

文字列テンプレート (英語)

Jython (英語)

Kotlin スクリプトテンプレート (GitHub)

Kotlin (英語)

他のスクリプトエンジンを統合するための基本的なルールは、ScriptEngine および Invocable インターフェースを実装する必要があるということです。
要件

クラスパスにスクリプトエンジンが必要です。詳細はスクリプトエンジンによって異なります。

  • Nashorn (英語) JavaScript エンジンには Java 8+ が付属しています。利用可能な最新の更新リリースを使用することを強くお勧めします。

  • JRuby (英語) は、Ruby サポートの依存関係として追加する必要があります。

  • Jython (英語) は、Python サポートの依存関係として追加する必要があります。

  • Kotlin スクリプトをサポートするには、org.jetbrains.kotlin:kotlin-script-util 依存関係と org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 行を含む META-INF/services/javax.script.ScriptEngineFactory ファイルを追加する必要があります。詳細については、この例 (GitHub) を参照してください。

スクリプトテンプレートライブラリが必要です。Javascript でこれを行う 1 つの方法は、WebJars (英語) を使用することです。

スクリプトテンプレート

ScriptTemplateConfigurer Bean を宣言して、使用するスクリプトエンジン、ロードするスクリプトファイル、テンプレートをレンダリングするために呼び出す関数などを指定できます。次の例では、Mustache テンプレートと Nashorn JavaScript エンジンを使用しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}

render 関数は、次のパラメーターで呼び出されます。

  • String template: テンプレートの内容

  • Map model: ビューモデル

  • RenderingContext renderingContext: アプリケーションコンテキスト、ロケール、テンプレートローダー、および URL へのアクセスを提供する RenderingContext(Javadoc) (5.0 以降)

Mustache.render() はこの署名とネイティブに互換性があるため、直接呼び出すことができます。

テンプレートテクノロジでカスタマイズが必要な場合は、カスタムレンダリング機能を実装するスクリプトを提供できます。例:Handlerbars (英語) は、使用する前にテンプレートをコンパイルする必要があり、サーバーサイドスクリプトエンジンでは利用できないブラウザー機能をエミュレートするために、ポリフィル (英語) が必要です。次の例は、カスタムレンダリング関数を設定する方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}
Nashorn で実行されている Handlebars や React など、同時実行用に設計されていないテンプレートライブラリで非スレッドセーフスクリプトエンジンを使用する場合は、sharedEngine プロパティを false に設定する必要があります。その場合、このバグ (英語) のため、Java SE 8 update 60 が必要ですが、通常は最新の Java SE パッチリリースを使用することをお勧めします。

polyfill.js は、次のスニペットが示すように、Handlebars が正しく実行するために必要な window オブジェクトのみを定義します。

var window = {};

この基本的な render.js 実装は、テンプレートを使用する前にコンパイルします。また、本番対応の実装では、キャッシュされたテンプレートまたはプリコンパイルされたテンプレートを保存して再利用する必要があります。これは、必要なカスタマイズ(たとえば、テンプレートエンジンの構成の管理)だけでなく、スクリプト側でも実行できます。次の例は、テンプレートのコンパイル方法を示しています。

function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}

その他の構成例については、Spring Framework 単体テスト、Java (GitHub) 、およびリソース (GitHub) を参照してください。

1.9.4. JSON と XML

コンテンツネゴシエーションの目的では、クライアントがリクエストするコンテンツタイプに応じて、HTML テンプレートを使用してモデルをレンダリングするか、他の形式(JSON や XML など)を交互にレンダリングできると便利です。これをサポートするために、Spring WebFlux は HttpMessageWriterView を提供します。これを使用して、Jackson2JsonEncoderJackson2SmileEncoder や Jaxb2XmlEncoder など、spring-web から利用可能なコーデックをプラグインすることができます。

他のビューテクノロジーとは異なり、HttpMessageWriterView は ViewResolver を必要とせず、代わりにデフォルトビューとして設定されます。異なる HttpMessageWriter インスタンスまたは Encoder インスタンスをラップして、このようなデフォルトビューを 1 つ以上構成できます。リクエストされたコンテンツタイプに一致するものが実行時に使用されます。

ほとんどの場合、モデルには複数の属性が含まれます。どれを直列化するかを決定するために、レンダリングに使用するモデル属性の名前で HttpMessageWriterView を構成できます。モデルに含まれる属性が 1 つだけの場合、その属性が使用されます。

1.10. HTTP キャッシング

HTTP キャッシングは、Web アプリケーションのパフォーマンスを大幅に改善できます。HTTP キャッシングは、Cache-Control レスポンスヘッダーと、Last-Modified や ETag などの後続の条件付きリクエストヘッダーを中心に展開します。Cache-Control は、プライベート(ブラウザなど)およびパブリック(プロキシなど)のキャッシュが、レスポンスをキャッシュして再利用する方法をアドバイスします。コンテンツが変更されていない場合、ETag ヘッダーを使用して、本文のない 304(NOT_MODIFIED)になる条件付きリクエストを作成します。ETag は、Last-Modified ヘッダーの後継として洗練されたものと見なすことができます。

このセクションでは、Spring WebFlux で使用可能な HTTP キャッシュ関連オプションについて説明します。

1.10.1. CacheControl

CacheControl(Javadoc) は、Cache-Control ヘッダーに関連する設定の構成をサポートし、多くの場所で引数として受け入れられます。

RFC 7234 (英語) は Cache-Control レスポンスヘッダーのすべての可能なディレクティブを記述しますが、CacheControl タイプは、次の例に示すように、一般的なシナリオに焦点を当てたユースケース指向のアプローチを取ります。

Java
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
Kotlin
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)

// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()

1.10.2. コントローラー

コントローラーは、HTTP キャッシングの明示的なサポートを追加できます。リソースの lastModified または ETag 値は、条件付きリクエストヘッダーと比較する前に計算する必要があるため、そうすることをお勧めします。次の例に示すように、コントローラーは ETag および Cache-Control 設定を ResponseEntity に追加できます。

Java
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book);
}
Kotlin
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {

    val book = findBook(id)
    val version = book.getVersion()

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book)
}

前述の例では、条件付きリクエストヘッダーとの比較でコンテンツが変更されていないことが示された場合、空のボディを含む 304(NOT_MODIFIED)レスポンスが送信されます。それ以外の場合、ETag および Cache-Control ヘッダーがレスポンスに追加されます。

次の例に示すように、コントローラーの条件付きリクエストヘッダーに対してチェックを行うこともできます。

Java
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

    long eTag = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
1 アプリケーション固有の計算。
2 レスポンスは 304(NOT_MODIFIED)に設定されています。それ以上の処理はありません。
3 リクエスト処理を続行します。
Kotlin
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {

    val eTag: Long = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null(2)
    }

    model.addAttribute(...) (3)
    return "myViewName"
}
1 アプリケーション固有の計算。
2 レスポンスは 304(NOT_MODIFIED)に設定されています。それ以上の処理はありません。
3 リクエスト処理を続行します。

eTag 値、lastModified 値、またはその両方に対して条件付きリクエストをチェックするための 3 つのバリアントがあります。条件付き GET および HEAD リクエストの場合、レスポンスを 304(NOT_MODIFIED)に設定できます。条件付き POSTPUT および DELETE の場合、代わりにレスポンスを 412(PRECONDITION_FAILED)に設定して、同時変更を防ぐことができます。

1.10.3. 静的リソース

最適なパフォーマンスを得るには、Cache-Control と条件付きレスポンスヘッダーで静的リソースを提供する必要があります。静的リソースの構成に関するセクションを参照してください。

1.11. WebFlux 構成

WebFlux Java 構成は、アノテーション付きコントローラーまたは関数エンドポイントでリクエストを処理するために必要なコンポーネントを宣言し、構成をカスタマイズするための API を提供します。つまり、Java 構成によって作成された基礎となる Bean を理解する必要はありません。ただし、理解したい場合は、WebFluxConfigurationSupport で表示するか、特別な Bean タイプの内容について詳しく読むことができます。

構成 API にはない高度なカスタマイズについては、拡張構成モードを使用して構成を完全に制御できます。

1.11.1. WebFlux 構成の有効化

次の例に示すように、Java config で @EnableWebFlux アノテーションを使用できます。

Java
@Configuration
@EnableWebFlux
public class WebConfig {
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig

上記の例では、多数の Spring WebFlux インフラストラクチャ Bean を登録し、JSON、XML などのクラスパスで利用可能な依存関係に適応しています。

1.11.2. WebFlux 構成 API

Java 構成では、次の例に示すように、WebFluxConfigurer インターフェースを実装できます。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // Implement configuration methods...
}

1.11.3. 変換、フォーマット

デフォルトでは、さまざまな数値および日付タイプのフォーマッタがインストールされ、フィールドでの @NumberFormat および @DateTimeFormat によるカスタマイズのサポートがインストールされています。

Java config でカスタムフォーマッタとコンバーターを登録するには、次を使用します。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}

デフォルトでは、Spring WebFlux は、日付値を解析およびフォーマットするときに、リクエストロケールを考慮します。これは、日付が「入力」フォームフィールドを持つ文字列として表されるフォームで機能します。ただし、「日付」および「時刻」フォームフィールドの場合、ブラウザは HTML 仕様で定義されている固定形式を使用します。このような場合、日付と時刻のフォーマットは次のようにカスタマイズできます。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
FormatterRegistrar 実装をいつ使用するかの詳細については、FormatterRegistrar SPI および FormattingConversionServiceFactoryBean を参照してください。

1.11.4. 検証

デフォルトでは、Bean バリデーションがクラスパスに存在する場合(たとえば、Hibernate バリデーター)、LocalValidatorFactoryBean は、@Controller メソッド引数の @Valid および @Validated で使用するためのグローバルバリデーターとして登録されます。

Java 構成では、次の例に示すように、グローバル Validator インスタンスをカスタマイズできます。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator(); {
        // ...
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun getValidator(): Validator {
        // ...
    }

}

次の例に示すように、Validator 実装をローカルに登録することもできます。

Java
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}
Kotlin
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}
LocalValidatorFactoryBean をどこかに挿入する必要がある場合は、Bean を作成し、@Primary でマークして、MVC config で宣言されたものとの競合を回避します。

1.11.5. コンテンツタイプリゾルバー

Spring WebFlux が、リクエストから @Controller インスタンスのリクエストされたメディアタイプを決定する方法を設定できます。デフォルトでは、Accept ヘッダーのみがチェックされますが、クエリパラメーターベースの戦略を有効にすることもできます。

次の例は、リクエストされたコンテンツタイプの解決をカスタマイズする方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
        // ...
    }
}

1.11.6. HTTP メッセージコーデック

次の例は、リクエストとレスポンスの本文の読み取りおよび書き込み方法をカスタマイズする方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024);
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // ...
    }
}

ServerCodecConfigurer は、デフォルトのリーダーとライターのセットを提供します。これを使用して、リーダーとライターを追加したり、デフォルトのものをカスタマイズしたり、デフォルトのものを完全に置き換えたりできます。

Jackson JSON および XML の場合、Jackson2ObjectMapperBuilder(Javadoc) の使用を検討してください。Jackson2ObjectMapperBuilder(Javadoc) は、Jackson のデフォルトプロパティを次のようにカスタマイズします。

また、次の既知のモジュールがクラスパスで検出された場合、自動的に登録します。

1.11.7. リゾルバーを表示

次の例は、ビューリゾルバーを構成する方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // ...
    }
}

ViewResolverRegistry には、Spring Framework が統合するビューテクノロジーへのショートカットがあります。次の例では、FreeMarker を使用しています(これには、基になる FreeMarker ビューテクノロジの構成も必要です)

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure Freemarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure Freemarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
    }
}

次の例に示すように、ViewResolver 実装をプラグインすることもできます。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        ViewResolver resolver = ... ;
        registry.viewResolver(resolver);
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        val resolver: ViewResolver = ...
        registry.viewResolver(resolver
    }
}

コンテンツネゴシエーションをサポートし、ビューリゾルバー (HTML 以外) を介して他の形式をレンダリングするには、spring-web から利用可能なコーデックのいずれかを受け入れる HttpMessageWriterView 実装に基づいて、1 つまたは複数の既定のビューを構成できます。次の例は、その方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {


    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
        registry.defaultViews(new HttpMessageWriterView(encoder));
    }

    // ...
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {


    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()

        val encoder = Jackson2JsonEncoder()
        registry.defaultViews(HttpMessageWriterView(encoder))
    }

    // ...
}

Spring WebFlux と統合されているビューテクノロジーの詳細については、ビューテクノロジーを参照してください。

1.11.8. 静的リソース

このオプションは、Resource(Javadoc) ベースの場所のリストから静的リソースを提供する便利な方法を提供します。

次の例では、/resources で始まるリクエストを指定すると、相対パスを使用して、クラスパス上の /static に関連する静的リソースを見つけて処理します。ブラウザーのキャッシュを最大限に使用し、ブラウザーが行う HTTP リクエストを削減するために、リソースには 1 年間の有効期限があります。Last-Modified ヘッダーも評価され、存在する場合は 304 ステータスコードが返されます。次のリストに例を示します。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
    }
}

リソースハンドラーは、ResourceResolver(Javadoc) 実装と ResourceTransformer(Javadoc) 実装のチェーンもサポートしています。これらを使用して、最適化されたリソースを操作するためのツールチェーンを作成できます。

コンテンツ、固定アプリケーションバージョン、またはその他の情報から計算された MD5 ハッシュに基づいて、バージョン管理されたリソース URL に VersionResourceResolver を使用できます。ContentVersionStrategy (MD5 ハッシュ)は、いくつかの注目すべき例外(モジュールローダーで使用される JavaScript リソースなど)がある場合に適しています。

次の例は、Java 構成で VersionResourceResolver を使用する方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }

}

ResourceUrlProvider を使用して URL を書き換え、リゾルバーとトランスフォーマーの完全なチェーンを適用できます(たとえば、バージョンを挿入するため)。WebFlux 構成は、ResourceUrlProvider を提供するため、他に注入することができます。

Spring MVC とは異なり、現在、WebFlux では、リゾルバーとトランスフォーマーのノンブロッキングチェーンを使用できるビューテクノロジーがないため、静的リソース URL を透過的に書き換える方法はありません。ローカルリソースのみを提供する場合、回避策は、ResourceUrlProvider を直接(たとえば、カスタム要素を介して)使用してブロックすることです。

EncodedResourceResolver (たとえば、Gzip、Brotli エンコード)と VersionedResourceResolver の両方を使用する場合、エンコードされていないファイルに基づいてコンテンツベースのバージョンが常に確実に計算されるように、この順序で登録する必要があります。

WebJars (英語) は、org.webjars:webjars-locator-core ライブラリがクラスパスに存在するときに自動的に登録される WebJarsResourceResolver でもサポートされます。リゾルバーは、jar のバージョンを含むように URL を書き換えることができ、バージョンのない受信 URL(たとえば、/jquery/jquery.min.js から /jquery/1.2.0/jquery.min.js など)と照合することもできます。

1.11.9. パスマッチング

パスマッチングに関連するオプションをカスタマイズできます。個々のオプションの詳細については、PathMatchConfigurer(Javadoc) javadoc を参照してください。次の例は、PathMatchConfigurer の使用方法を示しています。

Java
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController.class));
    }
}
Kotlin
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController::class.java))
    }
}

Spring WebFlux は、デコードされたパスセグメント値にアクセスするために、RequestPath と呼ばれるリクエストパスの解析された表現に依存し、セミコロンコンテンツ(つまり、パスまたはマトリックス変数)を削除します。つまり、Spring MVC とは異なり、リクエストパスをデコードするか、パスマッチングのためにセミコロンコンテンツを削除するかを指定する必要はありません。

Spring WebFlux はまた、また、されている Spring MVC、とは異なり、接尾辞パターンマッチングをサポートしていないお勧めします、それに離れ依存から移動します。

1.11.10. 拡張構成モード

@EnableWebFlux は DelegatingWebFluxConfiguration をインポートします:

  • WebFlux アプリケーションにデフォルトの Spring 構成を提供する

  • WebFluxConfigurer 実装を検出して委譲し、その構成をカスタマイズします。

拡張モードの場合、次の例に示すように、WebFluxConfigurer を実装する代わりに、@EnableWebFlux を削除して DelegatingWebFluxConfiguration から直接拡張できます。

Java
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
Kotlin
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}

既存のメソッドを WebConfig に保持できますが、ベースクラスからの Bean 宣言をオーバーライドしても、クラスパスに他の WebMvcConfigurer 実装をいくつでも持つことができます。

1.12. HTTP/2

HTTP/2 は、Reactor、Netty、Tomcat、Jetty、および Undertow でサポートされています。ただし、サーバー構成に関連する考慮事項があります。詳細については、HTTP/2 wiki ページ (GitHub) を参照してください。

2. WebClient

Spring WebFlux には、HTTP リクエスト用のリアクティブでノンブロッキングの WebClient が含まれています。クライアントには、宣言的構成のためのリアクティブ型を持つ関数で流れるような API があります。リアクティブライブラリを参照してください。WebFlux クライアントとサーバーは、同じノンブロッキングコーデックに依存して、リクエストおよびレスポンスコンテンツをエンコードおよびデコードします。

内部的に WebClient は HTTP クライアントライブラリに委譲します。デフォルトでは、Reactor Netty (GitHub) を使用し、Jetty リアクティブ HttpClient (GitHub) の組み込みサポートがあり、その他は ClientHttpConnector を介してプラグインできます。

2.1. 構成

WebClient を作成する最も簡単な方法は、静的ファクトリメソッドの 1 つを使用することです。

  • WebClient.create()

  • WebClient.create(String baseUrl)

上記のメソッドは、Reactor Netty HttpClient をデフォルト設定で使用し、io.projectreactor.netty:reactor-netty がクラスパス上にあることを想定しています。

WebClient.builder() を追加オプションとともに使用することもできます。

  • uriBuilderFactory: ベース URL として使用するために UriBuilderFactory をカスタマイズしました。

  • defaultHeader: すべてのリクエストのヘッダー。

  • defaultCookie: リクエストごとの Cookie。

  • defaultRequestConsumer ですべてのリクエストをカスタマイズします。

  • filter: すべてのリクエストのクライアントフィルター。

  • exchangeStrategies: HTTP メッセージリーダー / ライターのカスタマイズ。

  • clientConnector: HTTP クライアントライブラリの設定。

次の例では、HTTP コーデックを構成しています。

Java
WebClient client = WebClient.builder()
        .exchangeStrategies(builder -> {
                return builder.codecs(codecConfigurer -> {
                    //...
                });
        })
        .build();
Kotlin
val webClient = WebClient.builder()
        .exchangeStrategies { strategies ->
            strategies.codecs {
                //...
            }
        }
        .build()

一度構築されると、WebClient インスタンスは不変です。ただし、次の例に示すように、元のインスタンスに影響を与えることなく、クローンを作成して変更されたコピーを作成できます。

Java
WebClient client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD
Kotlin
val client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build()

val client2 = client1.mutate()
        .filter(filterC).filter(filterD).build()

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

2.1.1. MaxInMemorySize

Spring WebFlux は、アプリケーションメモリの課題を回避するために、コーデックのメモリ内データのバッファリングの制限を構成します。デフォルトでは、これは 256KB に設定されており、ユースケースに十分でない場合は、次のように表示されます。

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

次のコードサンプルを使用して、すべてのデフォルトコーデックでこの制限を構成できます。

Java
WebClient webClient = WebClient.builder()
        .exchangeStrategies(builder ->
            builder.codecs(codecs ->
                codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
            )
        )
        .build();
Kotlin
val webClient = WebClient.builder()
    .exchangeStrategies { builder ->
            builder.codecs {
                it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
            }
    }
    .build()

2.1.2. Reactor Netty

Reactor Netty 設定をカスタマイズするには、事前に構成された HttpClient を提供するだけです:

Java
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
Kotlin
val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()
リソース

デフォルトでは、HttpClient は、イベントループスレッドや接続プールなど、reactor.netty.http.HttpResources に保持されているグローバル Reactor Netty リソースに参加します。これは、イベントループの同時実行には固定の共有リソースが優先されるため、推奨されるモードです。このモードでは、プロセスが終了するまでグローバルリソースがアクティブのままになります。

サーバーがプロセスとタイミングを合わせている場合、通常、明示的なシャットダウンの必要はありません。ただし、サーバーがインプロセスを開始または停止できる場合(たとえば、WAR としてデプロイされた Spring MVC アプリケーション)、globalResources=true (デフォルト)で ReactorResourceFactory タイプの Spring 管理 Bean を宣言して、Reactor Netty がグローバルであることを確認できます。次の例に示すように、Spring ApplicationContext が閉じられると、リソースはシャットダウンされます。

Java
@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}
Kotlin
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()

グローバル Reactor Netty リソースに参加しないことも選択できます。ただし、このモードでは、次の例に示すように、すべての Reactor Netty クライアントおよびサーバーインスタンスが共有リソースを使用することを保証する負担があります。

Java
@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false); (1)
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };

    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper); (2)

    return WebClient.builder().clientConnector(connector).build(); (3)
}
1 グローバルなリソースから独立したリソースを作成します。
2 リソースファクトリで ReactorClientHttpConnector コンストラクターを使用します。
3 コネクターを WebClient.Builder に差し込みます。
Kotlin
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
    isUseGlobalResources = false (1)
}

@Bean
fun webClient(): WebClient {

    val mapper: (HttpClient) -> HttpClient = {
        // Further customizations...
    }

    val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)

    return WebClient.builder().clientConnector(connector).build() (3)
}
1 グローバルなリソースから独立したリソースを作成します。
2 リソースファクトリで ReactorClientHttpConnector コンストラクターを使用します。
3 コネクターを WebClient.Builder に差し込みます。
タイムアウト

接続タイムアウトを構成するには:

Java
import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000));
Kotlin
import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .tcpConfiguration { it.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)}

読み取りおよび / または書き込みタイムアウト値を設定するには:

Java
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .tcpConfiguration(client ->
                client.doOnConnected(conn -> conn
                        .addHandlerLast(new ReadTimeoutHandler(10))
                        .addHandlerLast(new WriteTimeoutHandler(10))));
Kotlin
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler

val httpClient = HttpClient.create().tcpConfiguration {
    it.doOnConnected { conn -> conn
            .addHandlerLast(ReadTimeoutHandler(10))
            .addHandlerLast(WriteTimeoutHandler(10))
    }
}

2.1.3. Jetty

次の例は、Jetty HttpClient 設定をカスタマイズする方法を示しています。

Java
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
ClientHttpConnector connector = new JettyClientHttpConnector(httpClient);

WebClient webClient = WebClient.builder().clientConnector(connector).build();
Kotlin
val httpClient = HttpClient()
httpClient.cookieStore = ...
val connector = JettyClientHttpConnector(httpClient)

val webClient = WebClient.builder().clientConnector(connector).build();

デフォルトでは、HttpClient は独自のリソース(ExecutorByteBufferPoolScheduler)を作成し、プロセスが終了するか stop() が呼び出されるまでアクティブのままになります。

次の例に示すように、タイプ JettyResourceFactory の Spring 管理 Bean を宣言することにより、Jetty クライアント(およびサーバー)の複数のインスタンス間でリソースを共有し、Spring ApplicationContext が閉じられたときにリソースを確実にシャットダウンできます。

Java
@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...

    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory()); (1)

    return WebClient.builder().clientConnector(connector).build(); (2)
}
1 リソースファクトリで JettyClientHttpConnector コンストラクターを使用します。
2 コネクターを WebClient.Builder に差し込みます。
Kotlin
@Bean
fun resourceFactory() = JettyResourceFactory()

@Bean
fun webClient(): WebClient {

    val httpClient = HttpClient()
    // Further customizations...

    val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)

    return WebClient.builder().clientConnector(connector).build() (2)
}
1 リソースファクトリで JettyClientHttpConnector コンストラクターを使用します。
2 コネクターを WebClient.Builder に差し込みます。

2.2. retrieve()

retrieve() メソッドは、レスポンス本文を取得してデコードする最も簡単な方法です。次の例は、その方法を示しています。

Java
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
Kotlin
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .awaitBody<Person>()

次の例に示すように、レスポンスからデコードされたオブジェクトのストリームを取得することもできます。

Java
Flux<Quote> result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(Quote.class);
Kotlin
val result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlow<Quote>()

デフォルトでは、4xx または 5xx ステータスコードでのレスポンスは、WebClientResponseException または WebClientResponseException.BadRequestWebClientResponseException.NotFound などの HTTP ステータス固有のサブクラスの 1 つになります。次の例に示すように、onStatus メソッドを使用して、結果の例外をカスタマイズすることもできます。

Java
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { ... }
        .onStatus(HttpStatus::is5xxServerError) { ... }
        .awaitBody<Person>()

onStatus を使用する場合、レスポンスにコンテンツがあると予想される場合、onStatus コールバックはそれを消費する必要があります。そうでない場合は、コンテンツが自動的に排出され、リソースが解放されます。

2.3. exchange()

exchange() メソッドは、retrieve メソッドよりも多くの制御を提供します。次の例は retrieve() と同等ですが、ClientResponse へのアクセスも提供します。

Java
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .exchange()
        .flatMap(response -> response.bodyToMono(Person.class));
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .awaitExchange()
        .awaitBody<Person>()

このレベルでは、完全な ResponseEntity を作成することもできます。

Java
Mono<ResponseEntity<Person>> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .exchange()
        .flatMap(response -> response.toEntity(Person.class));
Kotlin
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .awaitExchange()
        .toEntity<Person>()

retrieve() とは異なり) exchange() では、4xx および 5xx レスポンスの自動エラー信号がないことに注意してください。ステータスコードを確認し、続行する方法を決定する必要があります。

retrieve() とは異なり、exchange() を使用する場合、シナリオ(成功、エラー、予期しないデータなど)に関係なく、レスポンスコンテンツを消費するのはアプリケーションの責任です。そうしないと、メモリリークが発生する可能性があります。ClientResponse の Javadoc には、本文を消費するために使用可能なすべてのオプションがリストされています。exchange() を使用する正当な理由がない限り、一般に retrieve() を使用することをお勧めします。exchange() を使用すると、レスポンスをどのように、またはどのように消費するかを決定する前にレスポンスステータスとヘッダーを確認できます。

2.4. リクエストボディー

リクエスト本体は、次の例に示すように、Mono や Kotlin コルーチン Deferred など、ReactiveAdapterRegistry によって処理される任意の非同期タイプからエンコードできます。

Java
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(personMono, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val personDeferred: Deferred<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body<Person>(personDeferred)
        .retrieve()
        .awaitBody<Unit>()

次の例に示すように、オブジェクトのストリームをエンコードすることもできます。

Java
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_STREAM_JSON)
        .body(personFlux, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val people: Flow<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(people)
        .retrieve()
        .awaitBody<Unit>()

または、実際の値がある場合は、次の例に示すように、bodyValue ショートカットメソッドを使用できます。

Java
Person person = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val person: Person = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .awaitBody<Unit>()

2.4.1. フォームデータ

フォームデータを送信するには、MultiValueMap<String, String> を本文として提供できます。コンテンツは FormHttpMessageWriter によって application/x-www-form-urlencoded に自動的に設定されることに注意してください。次の例は、MultiValueMap<String, String> の使用方法を示しています。

Java
MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val formData: MultiValueMap<String, String> = ...

client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .awaitBody<Unit>()

次の例に示すように、BodyInserters を使用してインラインでフォームデータを提供することもできます。

Java
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .awaitBody<Unit>()

2.4.2. マルチパートデータ

マルチパートデータを送信するには、パーツのコンテンツを表す Object インスタンスまたはパーツのコンテンツとヘッダーを表す HttpEntity インスタンスのいずれかである MultiValueMap<String, ?> を提供する必要があります。MultipartBodyBuilder は、マルチパートリクエストを準備するための便利な API を提供します。次の例は、MultiValueMap<String, ?> を作成する方法を示しています。

Java
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();
Kotlin
val builder = MultipartBodyBuilder().apply {
    part("fieldPart", "fieldValue")
    part("filePart1", new FileSystemResource("...logo.png"))
    part("jsonPart", new Person("Jason"))
    part("myPart", part) // Part from a server request
}

val parts = builder.build()

ほとんどの場合、各パーツに Content-Type を指定する必要はありません。コンテンツタイプは、シリアライズするために選択された HttpMessageWriter に基づいて、または Resource の場合はファイル拡張子に基づいて自動的に決定されます。必要に応じて、オーバーロードされたビルダー part メソッドのいずれかを使用して、各パーツに使用する MediaType を明示的に提供できます。

MultiValueMap を準備したら、次の例に示すように、WebClient に渡す最も簡単な方法は body メソッドを使用することです。

Java
MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
val builder: MultipartBodyBuilder = ...

client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .awaitBody<Unit>()

MultiValueMap に少なくとも 1 つの String 以外の値(通常のフォームデータ(つまり application/x-www-form-urlencoded)を表す場合もある)が含まれている場合、Content-Type を multipart/form-data に設定する必要はありません。これは、MultipartBodyBuilder を使用する場合に常に当てはまり、HttpEntity ラッパーが保証されます。

MultipartBodyBuilder の代替として、次の例に示すように、組み込みの BodyInserters を介して、インラインスタイルのマルチパートコンテンツを提供することもできます。

Java
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .bodyToMono(Void.class);
Kotlin
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .awaitBody<Unit>()

2.5. クライアントフィルター

次の例に示すように、リクエストを傍受および変更するために、WebClient.Builder を介してクライアントフィルター(ExchangeFilterFunction)を登録できます。

Java
WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();
Kotlin
val client = WebClient.builder()
        .filter { request, next ->

            val filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build()

            next.exchange(filtered)
        }
        .build()

これは、認証などの横断的な関心事に使用できます。次の例では、静的ファクトリ方式による基本認証にフィルターを使用しています。

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();
Kotlin
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication

val client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build()

フィルターはすべてのリクエストにグローバルに適用されます。特定のリクエストのフィルターの動作を変更するには、次の例に示すように、チェーンのすべてのフィルターからアクセスできるリクエスト属性を ClientRequest に追加します。

Java
WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("https://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }
Kotlin
val client = WebClient.builder()
            .filter { request, _ ->
        val usr = request.attributes()["myAttribute"];
        // ...
    }.build()

    client.get().uri("https://example.org/")
            .attribute("myAttribute", "...")
            .retrieve()
            .awaitBody<Unit>()

既存の WebClient を複製したり、新しいフィルターを挿入したり、すでに登録されているフィルターを削除したりすることもできます。次の例では、インデックス 0 に基本認証フィルターを挿入します。

Java
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();
Kotlin
val client = webClient.mutate()
        .filters { it.add(0, basicAuthentication("user", "password")) }
        .build()

2.6. 同期使用

WebClient は、結果の最後でブロックすることにより、同期スタイルで使用できます。

Java
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
Kotlin
val person = runBlocking {
    client.get().uri("/person/{id}", i).retrieve()
            .awaitBody<Person>()
}

val persons = runBlocking {
    client.get().uri("/persons").retrieve()
            .bodyToFlow<Person>()
            .toList()
}

ただし、複数の呼び出しを行う必要がある場合は、各レスポンスを個別にブロックするのを避け、代わりに結合された結果を待つ方が効率的です。

Java
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", person);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();
Kotlin
val data = runBlocking {
        val personDeferred = async {
            client.get().uri("/person/{id}", personId)
                    .retrieve().awaitBody<Person>()
        }

        val hobbiesDeferred = async {
            client.get().uri("/person/{id}/hobbies", personId)
                    .retrieve().bodyToFlow<Hobby>().toList()
        }

        mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
    }

上記は単なる一例です。多くのリモート呼び出しを作成するリアクティブパイプラインをまとめる他のパターンと演算子がたくさんあります。

Flux または Mono では、Spring MVC または Spring WebFlux コントローラーをブロックする必要はありません。コントローラーメソッドから結果のリアクティブ型を返すだけです。同じ原則が Kotlin コルーチンと Spring WebFlux にも適用されます。コントローラーメソッドで中断機能を使用するか、Flow を返すだけです。

2.7. テスト

WebClient を使用するコードをテストするには、OkHttp MockWebServer (GitHub) などのモック Web サーバーを使用できます。使用例については、Spring Framework テストスイートの WebClientIntegrationTests (GitHub) または OkHttp リポジトリの static-server (GitHub) サンプルを参照してください。

3. WebSocket

リファレンスドキュメントのこのパートでは、リアクティブスタック WebSocket メッセージングのサポートについて説明します。

3.1. WebSocket の概要

WebSocket プロトコルである RFC 6455 (英語) は、単一の TCP 接続を介してクライアントとサーバー間に全二重双方向通信チャネルを確立する標準化された方法を提供します。これは HTTP とは異なる TCP プロトコルですが、ポート 80 および 443 を使用し、既存のファイアウォールルールの再利用を可能にする HTTP 上で動作するように設計されています。

WebSocket の対話は、HTTP Upgrade ヘッダーを使用してアップグレードするか、この場合は WebSocket プロトコルに切り替える HTTP リクエストで始まります。次の例は、このような相互作用を示しています。

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1Upgrade ヘッダー。
2Upgrade 接続を使用します。

通常の 200 ステータスコードの代わりに、WebSocket をサポートするサーバーは、次のような出力を返します。

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 プロトコルスイッチ

ハンドシェイクに成功した後、HTTP アップグレードリクエストの基になる TCP ソケットは、クライアントとサーバーの両方に対して開いたままになり、メッセージの送受信を継続します。

WebSockets の動作方法の完全な導入は、このドキュメントの範囲外です。RFC 6455、HTML5 の WebSocket の章、または Web に関する多くの導入とチュートリアルのいずれかを参照してください。

WebSocket サーバーが Web サーバー(nginx など)の背後で実行されている場合は、WebSocket アップグレードリクエストを WebSocket サーバーに渡すように構成する必要がある可能性が高いことに注意してください。同様に、アプリケーションがクラウド環境で実行されている場合は、WebSocket サポートに関連するクラウドプロバイダーの指示を確認してください。

3.1.1. HTTP 対 WebSocket

WebSocket は HTTP 互換であるように設計されており、HTTP リクエストで始まりますが、2 つのプロトコルが非常に異なるアーキテクチャとアプリケーションプログラミングモデルにつながることを理解することが重要です。

HTTP および REST では、アプリケーションは多くの URL としてモデル化されます。アプリケーションと対話するために、クライアントはこれらの URL にアクセスし、リクエスト / レスポンススタイルを使用します。サーバーは、HTTP URL、メソッド、およびヘッダーに基づいてリクエストを適切なハンドラーにルーティングします。

対照的に、WebSockets では、通常、最初の接続用の URL は 1 つだけです。その後、すべてのアプリケーションメッセージは同じ TCP 接続で流れます。これは、まったく異なる非同期のイベント駆動型のメッセージングアーキテクチャを指します。

WebSocket は低レベルのトランスポートプロトコルでもあり、HTTP とは異なり、メッセージのコンテンツにセマンティクスを規定していません。つまり、クライアントとサーバーがメッセージのセマンティクスに同意しない限り、メッセージをルーティングまたは処理する方法はありません。

WebSocket クライアントおよびサーバーは、HTTP ハンドシェイクリクエストの Sec-WebSocket-Protocol ヘッダーを介して、より高いレベルのメッセージングプロトコル(たとえば、STOMP)の使用をネゴシエートできます。それがなければ、彼らは彼ら自身の規約を考え出す必要があります。

3.1.2. WebSockets を使用する場合

WebSockets は、Web ページを動的かつインタラクティブにすることができます。ただし、多くの場合、Ajax と HTTP ストリーミングまたはロングポーリングの組み合わせにより、シンプルで効果的なソリューションを提供できます。

例:ニュース、メール、ソーシャルフィードは動的に更新する必要がありますが、数分ごとに更新しても問題ありません。一方、コラボレーション、ゲーム、および金融アプリは、リアルタイムにさらに近づける必要があります。

遅延だけが決定要因ではありません。メッセージの量が比較的少ない場合(ネットワーク障害の監視など)、HTTP ストリーミングまたはポーリングは効果的なソリューションを提供できます。WebSocket の使用に最適なのは、低レイテンシー、高周波数、高ボリュームの組み合わせです。

また、インターネット上では、Upgrade ヘッダーを渡すように構成されていないか、アイドル状態であると思われる長寿命の接続を閉じるため、コントロール外の制限プロキシが WebSocket 相互作用を妨げる可能性があることに注意してください。これは、ファイアウォール内の内部アプリケーションに WebSocket を使用することは、一般公開アプリケーションよりも簡単な決定であることを意味します。

3.2. WebSocket API

Spring Framework は、WebSocket メッセージを処理するクライアント側およびサーバー側のアプリケーションを作成するために使用できる WebSocket API を提供します。

3.2.1. サーバー

WebSocket サーバーを作成するには、最初に WebSocketHandler を作成できます。次の例は、その方法を示しています。

Java
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // ...
    }
}
Kotlin
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession

class MyWebSocketHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        // ...
    }
}

次に、次の例に示すように、URL にマップして WebSocketHandlerAdapter を追加できます。

Java
@Configuration
class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());
        int order = -1; // before annotated controllers

        return new SimpleUrlHandlerMapping(map, order);
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}
Kotlin
@Configuration
class WebConfig {

    @Bean
    fun handlerMapping(): HandlerMapping {
        val map = mapOf("/path" to MyWebSocketHandler())
        val order = -1 // before annotated controllers

        return SimpleUrlHandlerMapping(map, order)
    }

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}

3.2.2. WebSocketHandler

WebSocketHandler の handle メソッドは、WebSocketSession を取得して Mono<Void> を返し、セッションのアプリケーション処理が完了したことを示します。セッションは、受信メッセージ用と送信メッセージ用の 2 つのストリームを介して処理されます。次の表に、ストリームを処理する 2 つのメソッドを示します。

WebSocketSession 法 説明

Flux<WebSocketMessage> receive()

受信メッセージストリームへのアクセスを提供し、接続が閉じられると完了します。

Mono<Void> send(Publisher<WebSocketMessage>)

送信メッセージのソースを取得し、メッセージを書き込み、ソースが完了して書き込みが完了すると完了する Mono<Void> を返します。

WebSocketHandler は、受信ストリームと送信ストリームを統合されたフローに構成し、そのフローの補完を反映する Mono<Void> を返す必要があります。アプリケーション要件に応じて、統合フローは次の場合に完了します。

  • 受信または送信のメッセージストリームが完了します。

  • 受信ストリームは完了します(つまり、接続が閉じられます)が、送信ストリームは無限です。

  • WebSocketSession の close メソッドを介して選択したポイント。

受信と送信のメッセージストリームが一緒に構成される場合、Reactive Streams は終了アクティビティを通知するため、接続が開いているかどうかを確認する必要はありません。受信ストリームは完了またはエラー信号を受信し、送信ストリームはキャンセル信号を受信します。

ハンドラーの最も基本的な実装は、受信ストリームを処理するものです。次の例は、このような実装を示しています。

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()            (1)
                .doOnNext(message -> {
                    // ...                  (2)
                })
                .concatMap(message -> {
                    // ...                  (3)
                })
                .then();                    (4)
    }
}
1 受信メッセージのストリームにアクセスします。
2 各メッセージで何かをします。
3 メッセージコンテンツを使用するネストされた非同期操作を実行します。
4 受信が完了したときに完了する Mono<Void> を返します。
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        return session.receive()            (1)
                .doOnNext {
                    // ...                  (2)
                }
                .concatMap {
                    // ...                  (3)
                }
                .then()                     (4)
    }
}
1 受信メッセージのストリームにアクセスします。
2 各メッセージで何かをします。
3 メッセージコンテンツを使用するネストされた非同期操作を実行します。
4 受信が完了したときに完了する Mono<Void> を返します。
ネストされた非同期操作の場合、プールされたデータバッファーを使用する基になるサーバー(たとえば、Netty)で message.retain() を呼び出す必要があります。そうしないと、データを読み取る前にデータバッファが解放される可能性があります。背景については、データバッファとコーデックを参照してください。

次の実装では、受信ストリームと送信ストリームを組み合わせます。

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Flux<WebSocketMessage> output = session.receive()               (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .map(value -> session.textMessage("Echo " + value));    (2)

        return session.send(output);                                    (3)
    }
}
1 受信メッセージストリームを処理します。
2 送信メッセージを作成し、結合されたフローを作成します。
3 受信を続けている間に完了しない Mono<Void> を返します。
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val output = session.receive()                      (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .map { session.textMessage("Echo $it") }    (2)

        return session.send(output)                         (3)
    }
}
1 受信メッセージストリームを処理します。
2 送信メッセージを作成し、結合されたフローを作成します。
3 受信を続けている間に完了しない Mono<Void> を返します。

次の例に示すように、受信ストリームと送信ストリームは独立しており、完了のためにのみ結合できます。

Java
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Mono<Void> input = session.receive()                                (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .then();

        Flux<String> source = ... ;
        Mono<Void> output = session.send(source.map(session::textMessage)); (2)

        return Mono.zip(input, output).then();                              (3)
    }
}
1 受信メッセージストリームを処理します。
2 送信メッセージを送信します。
3 ストリームに参加し、いずれかのストリームが終了すると完了する Mono<Void> を返します。
Kotlin
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val input = session.receive()                                   (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .then()

        val source: Flux<String> = ...
        val output = session.send(source.map(session::textMessage))     (2)

        return Mono.zip(input, output).then()                           (3)
    }
}
1 受信メッセージストリームを処理します。
2 送信メッセージを送信します。
3 ストリームに参加し、いずれかのストリームが終了すると完了する Mono<Void> を返します。

3.2.3. DataBuffer

DataBuffer は、WebFlux のバイトバッファーの表現です。参照の Spring コア部分は、データバッファとコーデックのセクションでさらに詳しく説明しています。理解すべき重要な点は、Netty のような一部のサーバーでは、バイトバッファーがプールされ、参照カウントされ、メモリリークを避けるために消費されたときに解放する必要があるということです。

Netty で実行する場合、アプリケーションが解放されないように入力データバッファーを保持する場合は、DataBufferUtils.retain(dataBuffer) を使用し、その後バッファーが消費されるときに DataBufferUtils.release(dataBuffer) を使用する必要があります。

3.2.4. ハンドシェーク

WebSocketHandlerAdapter は WebSocketService に委譲します。デフォルトでは、これは HandshakeWebSocketService のインスタンスであり、WebSocket リクエストで基本的なチェックを実行し、使用中のサーバーに RequestUpgradeStrategy を使用します。現在、Reactor、Netty、Tomcat、Jetty、および Undertow の組み込みサポートがあります。

HandshakeWebSocketService は Predicate<String> を設定して WebSession から属性を抽出し、WebSocketSession の属性に挿入できる sessionAttributePredicate プロパティを公開します。

3.2.5. サーバー構成

各サーバーの RequestUpgradeStrategy は、基盤となる WebSocket エンジンで使用可能な WebSocket 関連の構成オプションを公開します。次の例では、Tomcat で実行するときに WebSocket オプションを設定します。

Java
@Configuration
class WebConfig {

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter(webSocketService());
    }

    @Bean
    public WebSocketService webSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
Kotlin
@Configuration
class WebConfig {

    @Bean
    fun handlerAdapter() =
            WebSocketHandlerAdapter(webSocketService())

    @Bean
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}

サーバーのアップグレード戦略を確認して、使用可能なオプションを確認してください。現在、Tomcat と Jetty のみがこのようなオプションを公開しています。

3.2.6. CORS

CORS を設定し、WebSocket エンドポイントへのアクセスを制限する最も簡単な方法は、WebSocketHandler に CorsConfigurationSource を実装させ、許可された発信元、ヘッダー、その他の詳細を含む CorsConfiguraiton を返すことです。それができない場合は、SimpleUrlHandler の corsConfigurations プロパティを設定して、URL パターンで CORS 設定を指定することもできます。両方が指定されている場合は、CorsConfiguration で combine メソッドを使用して結合されます。

3.2.7. クライアント

Spring WebFlux は、Reactor、Netty、Tomcat、Jetty、Undertow、および標準 Java(つまり JSR-356)の実装で WebSocketClient 抽象化を提供します。

Tomcat クライアントは事実上、WebSocketSession 処理にいくつかの追加機能を備えた標準 Java の拡張であり、Tomcat 固有の API を利用してバックプレッシャーのメッセージの受信を一時停止します。

WebSocket セッションを開始するには、クライアントのインスタンスを作成し、その execute メソッドを使用できます。

Java
WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
                .doOnNext(System.out::println)
                .then());
Kotlin
val client = ReactorNettyWebSocketClient()

        val url = URI("ws://localhost:8080/path")
        client.execute(url) { session ->
            session.receive()
                    .doOnNext(::println)
            .then()
        }

Jetty などの一部のクライアントは Lifecycle を実装しており、使用する前に停止および開始する必要があります。すべてのクライアントには、基になる WebSocket クライアントの構成に関連するコンストラクターオプションがあります。

4. テスト

spring-test モジュールは、ServerHttpRequestServerHttpResponse および ServerWebExchange のモック実装を提供します。モックオブジェクトの説明については、Spring Web リアクティブを参照してください。

WebTestClient は、これらのモックリクエストおよびレスポンスオブジェクトに基づいて構築され、HTTP サーバーなしで WebFlux アプリケーションのテストをサポートします。エンドツーエンドの統合テストにも WebTestClient を使用できます。

5. RSocket

このセクションでは、Spring Framework の RSocket プロトコルのサポートについて説明します。

5.1. 概要

RSocket は、TCP、WebSocket、およびその他のバイトストリームトランスポートを介した多重化された二重通信用のアプリケーションプロトコルであり、次の相互作用モデルのいずれかを使用します。

  • Request-Response — 1 つのメッセージを送信し、1 つを受信します。

  • Request-Stream — 1 つのメッセージを送信し、メッセージのストリームを受信します。

  • Channel —  メッセージのストリームを両方向に送信します。

  • Fire-and-Forget —  一方向のメッセージを送信します。

最初の接続が確立されると、両側が対称になり、各側が上記の相互作用のいずれかを開始できるため、「クライアント」と「サーバー」の区別が失われます。これが、プロトコルで参加側を「リクエスター」および「レスポンダー」と呼び、上記の相互作用を「リクエストストリーム」または単に「リクエスト」と呼ぶ理由です。

RSocket プロトコルの主な機能と利点は次のとおりです。

  • Reactive Streams (英語) セマンティクスは、Request-Stream および Channel のようなストリーミングリクエストのためにネットワーク境界を横断し、バックプレッシャー信号は、リクエスタとレスポンダとの間を移動し、リクエスタがソースでレスポンダの速度を低下させることを可能にし、ネットワーク層輻輳制御への依存、およびネットワークレベルまたは任意のレベルでのバッファリングの必要性を低減します。

  • 調整をリクエストする  —  この機能は、LEASE フレームにちなんで「リース」という名前が付けられています。このフレームは、各エンドから送信して、特定の時間に他のエンドが許可するリクエストの総数を制限できます。リースは定期的に更新されます。

  • セッション再開  —  これは接続が失われるように設計されており、何らかの状態を維持する必要があります。状態管理はアプリケーションに対して透過的であり、可能な場合に生産者を停止し、必要な状態の量を減らすことができるバックプレッシャーと組み合わせてうまく機能します。

  • 大きなメッセージの断片化と再組み立て。

  • キープアライブ(ハートビート)。

RSocket は複数の言語で実装 (GitHub) されています。Java ライブラリ (GitHub) プロジェクト Reactor (英語) および Reactor Netty (GitHub) 上に構築されており、トランスポート用です。つまり、アプリケーションの Reactive Streams パブリッシャーからの信号は、RSocket を介してネットワーク全体に透過的に伝播します。

5.1.1. プロトコル

RSocket の利点の 1 つはそれがワイヤの上でよく定義された振舞いと何らかのプロトコル拡張 (GitHub) に伴う読みやすい仕様 (英語) を持っているということです。言語の実装や高レベルのフレームワーク API に関係なく、仕様を読むことをお勧めします。このセクションでは、コンテキストを確立するための簡潔な概要を提供します。

接続

最初に、クライアントは TCP や WebSocket などの低レベルのストリーミングトランスポートを介してサーバーに接続し、SETUP フレームをサーバーに送信して接続のパラメーターを設定します。

サーバーは SETUP フレームを拒否する場合がありますが、通常、送信(クライアント用)および受信(サーバー用)した後、SETUP がリースセマンティクスを使用してリクエストの数を制限しない限り、リクエストを開始できます。どちらの側も、リクエストを許可するために、もう一方の端からの LEASE フレームを待つ必要があります。

リクエストをする

接続が確立されると、両側でフレーム REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNEL または REQUEST_FNF のいずれかを介してリクエストを開始できます。これらの各フレームは、リクエスターからレスポンダーに 1 つのメッセージを運びます。

次に、レスポンダーはレスポンスメッセージとともに PAYLOAD フレームを返します。REQUEST_CHANNEL の場合、リクエスターはさらに多くのリクエストメッセージを含む PAYLOAD フレームを送信します。

リクエストに Request-Stream や Channel などのメッセージのストリームが含まれる場合、レスポンダーはリクエスターからのリクエスト信号を考慮する必要があります。需要はメッセージの数として表されます。初期需要は、REQUEST_STREAM および REQUEST_CHANNEL フレームで指定されます。後続のリクエストは、REQUEST_N フレームを介して通知されます。

各側は、METADATA_PUSH フレームを介して、個々のリクエストではなく、接続全体に関するメタデータ通知も送信できます。

メッセージフォーマット

RSocket メッセージにはデータとメタデータが含まれます。メタデータは、ルートやセキュリティトークンなどを送信するために使用できます。データとメタデータは異なる形式にすることができます。それぞれの MIME タイプは SETUP フレームで宣言され、特定の接続のすべてのリクエストに適用されます。

すべてのメッセージにメタデータを含めることができますが、通常、ルートなどのメタデータはリクエストごとであるため、リクエストの最初のメッセージ、つまりフレーム REQUEST_RESPONSEREQUEST_STREAMREQUEST_CHANNEL または REQUEST_FNF の 1 つにのみ含まれます。

プロトコル拡張機能は、アプリケーションで使用する一般的なメタデータ形式を定義します。

5.1.2. Java 実装

RSocket の Java 実装 (GitHub) プロジェクト Reactor (英語) 上に構築されています。TCP および WebSocket のトランスポートは Reactor Netty (GitHub) に基づいています。Reactive Streams ライブラリとして、Reactor はプロトコルを実装する作業を簡素化します。アプリケーションでは、宣言演算子と透過的なバックプレッシャーサポートを備えた Flux および Mono を使用するのが自然です。

RSocket Java の API は、意図的に最小限で基本的なものです。プロトコル機能に焦点を当てており、アプリケーションプログラミングモデル(RPC codegen vs other など)をより高いレベルの独立した関心事として残しています。

メイン契約 io.rsocket.RSocket (GitHub) は、単一のメッセージの約束を表す Mono、メッセージのストリームを表す Flux、およびバイトバッファーとしてデータとメタデータにアクセスする実際のメッセージを表す io.rsocket.Payload を使用して、4 つのリクエスト対話タイプをモデル化します。RSocket 契約は対称的に使用されます。リクエストの場合、アプリケーションには、リクエストを実行する RSocket が与えられます。応答のために、アプリケーションは RSocket を実装してリクエストを処理します。

これは、徹底的な導入を意図したものではありません。ほとんどの場合、Spring アプリケーションはその API を直接使用する必要はありません。ただし、Spring に依存しない RSocket を確認または実験することが重要な場合があります。RSocket Java リポジトリには、API とプロトコルの機能を示す多数のサンプルアプリ (GitHub) が含まれています。

5.1.3. Spring サポート

spring-messaging モジュールには次のものが含まれます。

  • RSocketRequester  —  データとメタデータのエンコード / デコードを使用して io.rsocket.RSocket を介してリクエストを行うための流れるような API。

  • アノテーション付きレスポンダー  — @MessageMapping は、応答用のアノテーション付きハンドラーメソッドです。

spring-web モジュールには、Jackson CBOR/JSON などの Encoder および Decoder 実装、および RSocket アプリケーションが必要とする可能性が高い Protobuf が含まれています。また、効率的なルートマッチングのためにプラグインできる PathPatternParser も含まれています。

Spring Boot 2.2 は、TCP または WebSocket を介した RSocket サーバーの立ち上げをサポートしています。これには、WebFlux サーバーで WebSocket を介して RSocket を公開するオプションも含まれます。RSocketRequester.Builder および RSocketStrategies のクライアントサポートと自動構成もあります。詳細については、Spring Boot リファレンスの RSocket セクションを参照してください。

Spring Security 5.2 は RSocket サポートを提供します。

Spring Integration 5.2 は、RSocket クライアントおよびサーバーと対話するための受信および送信ゲートウェイを提供します。詳細については、Spring Integration リファレンスマニュアルを参照してください。

Spring Cloud Gateway は RSocket 接続をサポートしています。

5.2. RSocketRequester

RSocketRequester は、RSocket リクエストを実行するための流れるような API を提供し、低レベルのデータバッファーではなく、データとメタデータのオブジェクトを受け入れて返します。対称的に使用して、クライアントからリクエストを作成したり、サーバーからリクエストを作成したりできます。

5.2.1. クライアントリクエスター

クライアント側で RSocketRequester を取得するには、最初の RSocket SETUP フレームを準備して送信するとともに、サーバーに接続する必要があります。RSocketRequester はそのためのビルダーを提供します。内部的には io.rsocket.core.RSocketConnector 上に構築されています。

これは、デフォルト設定で接続する最も基本的な方法です。

Java
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .connectTcp("localhost", 7000);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .connectWebSocket(URI.create("https://example.org:8080/rsocket"));
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait
import org.springframework.messaging.rsocket.connectWebSocketAndAwait

val requester = RSocketRequester.builder()
        .connectTcpAndAwait("localhost", 7000)

val requester = RSocketRequester.builder()
        .connectWebSocketAndAwait(URI.create("https://example.org:8080/rsocket"))

上記は延期されます。リクエスターを実際に接続して使用するには:

Java
// Connect asynchronously
RSocketRequester.builder().connectTcp("localhost", 7000)
    .subscribe(requester -> {
        // ...
    });

// Or block
RSocketRequester requester = RSocketRequester.builder()
    .connectTcp("localhost", 7000)
    .block(Duration.ofSeconds(5));
Kotlin
// Connect asynchronously
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

    private var requester: RSocketRequester? = null

    private suspend fun requester() = requester ?:
        RSocketRequester.builder().connectTcpAndAwait("localhost", 7000).also { requester = it }

    suspend fun doSomething() = requester().route(...)
}

// Or block
import org.springframework.messaging.rsocket.connectTcpAndAwait

class MyService {

    private val requester = runBlocking {
        RSocketRequester.builder().connectTcpAndAwait("localhost", 7000)
    }

    suspend fun doSomething() = requester.route(...)
}
接続設定

RSocketRequester.Builder は、初期 SETUP フレームをカスタマイズするために以下を提供します。

  • dataMimeType(MimeType) —  接続上のデータの MIME タイプを設定します。

  • metadataMimeType(MimeType) —  接続のメタデータの MIME タイプを設定します。

  • setupData(Object) —  SETUP に含めるデータ。

  • setupRoute(String, Object…​) —  SETUP に含めるメタデータのルート。

  • setupMetadata(Object, MimeType) —  SETUP に含める他のメタデータ。

データの場合、デフォルトの MIME タイプは最初に構成された Decoder から派生します。メタデータの場合、デフォルトの MIME タイプは複合メタデータ (GitHub) であり、リクエストごとに複数のメタデータ値と MIME タイプのペアを許可します。通常、両方を変更する必要はありません。

SETUP フレームのデータとメタデータはオプションです。サーバー側では、@ConnectMapping メソッドを使用して、接続の開始と SETUP フレームのコンテンツを処理できます。メタデータは、接続レベルのセキュリティに使用できます。

戦略

RSocketRequester.Builder は RSocketStrategies を受け入れて、リクエスターを構成します。これを使用して、データおよびメタデータ値の(de)-serialization のエンコーダーおよびデコーダーを提供する必要があります。デフォルトでは、Stringbyte[] および ByteBuffer の spring-core からの基本コーデックのみが登録されます。spring-web を追加すると、次のように登録できるより多くのものにアクセスできます。

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
    .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
    .build();

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketStrategies(strategies)
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
        .encoders { it.add(Jackson2CborEncoder()) }
        .decoders { it.add(Jackson2CborDecoder()) }
        .build()

val requester = RSocketRequester.builder()
        .rsocketStrategies(strategies)
        .connectTcpAndAwait("localhost", 7000)

RSocketStrategies は再利用のために設計されています。いくつかのシナリオでは、たとえば同じアプリケーションのクライアントとサーバーの場合、Spring 構成で宣言することをお勧めします。

クライアントレスポンダー

RSocketRequester.Builder を使用して、サーバーからのリクエストに対するレスポンダーを構成できます。

サーバーで使用されているものと同じインフラストラクチャに基づいてクライアント側の応答にアノテーション付きハンドラーを使用できますが、次のようにプログラムで登録します。

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .routeMatcher(new PathPatternRouteMatcher())  (1)
    .build();

SocketAcceptor responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(responder)) (3)
    .connectTcp("localhost", 7000);
1spring-web が存在する場合は、効率的なルートマッチングのために PathPatternRouteMatcher を使用します。
2@MessageMaping および / または @ConnectMapping メソッドを使用して、クラスからレスポンダを作成します。
3 レスポンダを登録します。
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val strategies = RSocketStrategies.builder()
        .routeMatcher(PathPatternRouteMatcher())  (1)
        .build()

val responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(responder) } (3)
        .connectTcpAndAwait("localhost", 7000)
1spring-web が存在する場合は、効率的なルートマッチングのために PathPatternRouteMatcher を使用します。
2@MessageMaping および / または @ConnectMapping メソッドを使用して、クラスからレスポンダを作成します。
3 レスポンダを登録します。

上記は、クライアントレスポンダのプログラムによる登録用に設計されたショートカットにすぎないことに注意してください。クライアントレスポンダーが Spring 構成にある代替シナリオの場合、RSocketMessageHandler を Spring Bean として宣言し、次のように適用できます。

Java
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(handler.responder()))
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.beans.factory.getBean
import org.springframework.messaging.rsocket.connectTcpAndAwait

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(handler.responder()) }
        .connectTcpAndAwait("localhost", 7000)

上記の場合、RSocketMessageHandler で setHandlerPredicate を使用して、クライアントレスポンダーを検出するための別の戦略に切り替える必要がある場合もあります。@RSocketClientResponder などのカスタムアノテーションとデフォルトの @Controller に基づいています。これは、クライアントとサーバー、または同じアプリケーション内の複数のクライアントを使用するシナリオで必要です。

プログラミングモデルの詳細については、アノテーション付きレスポンダーも参照してください。

拡張

RSocketRequesterBuilder は、基になる io.rsocket.core.RSocketConnector を公開するためのコールバックを提供し、キープアライブインターバル、セッション再開、インターセプターなどの詳細設定オプションを提供します。次のように、そのレベルでオプションを構成できます。

Java
Mono<RSocketRequester> requesterMono = RSocketRequester.builder()
    .rsocketConnector(connector -> {
        // ...
    })
    .connectTcp("localhost", 7000);
Kotlin
import org.springframework.messaging.rsocket.connectTcpAndAwait

val requester = RSocketRequester.builder()
        .rsocketConnector {
            //...
        }.connectTcpAndAwait("localhost", 7000)

5.2.2. サーバーリクエスター

サーバーから接続されたクライアントにリクエストを行うことは、サーバーから接続されたクライアントのリクエスターを取得することです。

アノテーション付きレスポンダーでは、@ConnectMapping および @MessageMapping メソッドは RSocketRequester 引数をサポートします。これを使用して、接続のリクエスターにアクセスします。@ConnectMapping メソッドは本質的に SETUP フレームのハンドラーであり、リクエストを開始する前に処理する必要があることに注意してください。そのため、最初のリクエストは処理から分離する必要があります。例:

Java
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
    requester.route("status").data("5")
        .retrieveFlux(StatusReport.class)
        .subscribe(bar -> { (1)
            // ...
        });
    return ... (2)
}
1 処理とは無関係に、リクエストを非同期的に開始します。
2 処理を実行し、完了 Mono<Void> を返します。
Kotlin
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
    GlobalScope.launch {
        requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
            // ...
        }
    }
    /// ... (2)
}
1 処理とは無関係に、リクエストを非同期的に開始します。
2 サスペンド機能で処理してください。

5.2.3. 要求

クライアントまたはサーバーのリクエスターを取得したら、次のようにリクエストを作成できます。

Java
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlux(AirportLocation.class); (3)
1 リクエストメッセージのメタデータに含めるルートを指定します。
2 リクエストメッセージのデータを提供します。
3 予想されるレスポンスを宣言します。
Kotlin
val viewBox: ViewBox = ...

val locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlow<AirportLocation>() (3)
1 リクエストメッセージのメタデータに含めるルートを指定します。
2 リクエストメッセージのデータを提供します。
3 予想されるレスポンスを宣言します。

相互作用タイプは、入力と出力のカーディナリティから暗黙的に決定されます。上記の例は、1 つの値が送信され、値のストリームが受信されるため、Request-Stream です。ほとんどの場合、入力と出力の選択が RSocket インタラクションの種類と、レスポンダーが期待する入力と出力の種類と一致する限り、これについて考える必要はありません。無効な組み合わせの唯一の例は、多対 1 です。

data(Object) メソッドは、Flux および Mono を含む Reactive Streams Publisher、および ReactiveAdapterRegistry に登録されている値のその他のプロデューサーも受け入れます。同じタイプの値を生成する Flux などの複数値 Publisher の場合、オーバーロードされた data メソッドのいずれかを使用して、すべての要素のタイプチェックと Encoder ルックアップを回避することを検討してください。

data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);

data(Object) ステップはオプションです。データを送信しないリクエストの場合はスキップします。

Java
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
    .retrieveMono(AirportLocation.class);
Kotlin
import org.springframework.messaging.rsocket.retrieveAndAwait

val location = requester.route("find.radar.EWR")
    .retrieveAndAwait<AirportLocation>()

複合メタデータ (GitHub) (デフォルト)を使用し、値が登録済み Encoder でサポートされている場合、追加のメタデータ値を追加できます。例:

Java
String securityToken = ... ;
ViewBox viewBox = ... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");

Flux<AirportLocation> locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlux(AirportLocation.class);
Kotlin
import org.springframework.messaging.rsocket.retrieveFlow

val requester: RSocketRequester = ...

val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")

val locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlow<AirportLocation>()

Fire-and-Forget の場合、Mono<Void> を返す send() メソッドを使用します。Mono は、メッセージが正常に送信されたことのみを示し、処理されたことを示すものではないことに注意してください。

5.3. アノテーション付きレスポンダー

RSocket レスポンダーは、@MessageMapping および @ConnectMapping メソッドとして実装できます。@MessageMapping メソッドは個々のリクエストを処理し、@ConnectMapping メソッドは接続レベルのイベント(セットアップとメタデータプッシュ)を処理します。アノテーション付きのレスポンダーは、サーバー側からの応答とクライアント側からの応答のために対称的にサポートされます。

5.3.1. サーバーレスポンダー

サーバー側でアノテーション付きのレスポンダーを使用するには、RSocketMessageHandler を Spring 構成に追加して、@MessageMapping および @ConnectMapping メソッドで @Controller Bean を検出します。

Java
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}
Kotlin
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        routeMatcher = PathPatternRouteMatcher()
    }
}

次に、Java RSocket API を使用して RSocket サーバーを起動し、レスポンダーの RSocketMessageHandler を次のように接続します。

Java
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
    RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .block();
Kotlin
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val server = RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .awaitFirst()

RSocketMessageHandler は、デフォルトで複合 (GitHub) メタデータとルーティング (GitHub) メタデータをサポートしています。別の MIME タイプに切り替えるか、追加のメタデータ MIME タイプを登録する必要がある場合は、MetadataExtractor を設定できます。

サポートするメタデータおよびデータ形式に必要な Encoder および Decoder インスタンスを設定する必要があります。コーデックの実装には、spring-web モジュールが必要になる可能性があります。

デフォルトでは、SimpleRouteMatcher は AntPathMatcher を介したルートのマッチングに使用されます。効率的なルートマッチングのために、spring-web から PathPatternRouteMatcher を差し込むことをお勧めします。RSocket ルートは階層化できますが、URL パスではありません。両方のルートマッチャーが「.」を使用するように構成されています。デフォルトではセパレータとして使用され、HTTP URL のような URL デコードはありません。

RSocketMessageHandler は、同じプロセスでクライアントとサーバー間で構成を共有する必要がある場合に便利な RSocketStrategies を介して構成できます。

Java
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
            .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
            .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
            .routeMatcher(new PathPatternRouteMatcher())
            .build();
    }
}
Kotlin
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
            .encoders { it.add(Jackson2CborEncoder()) }
            .decoders { it.add(Jackson2CborDecoder()) }
            .routeMatcher(PathPatternRouteMatcher())
            .build()
}

5.3.2. クライアントレスポンダー

クライアント側のアノテーション付き応答者は、RSocketRequester.Builder で構成する必要があります。詳細については、クライアントレスポンダーを参照してください。

5.3.3. @MessageMapping

サーバーまたはクライアントレスポンダーの設定が完了したら、@MessageMapping メソッドを次のように使用できます。

Java
@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        // ...
    }
}
Kotlin
@Controller
class RadarsController {

    @MessageMapping("locate.radars.within")
    fun radars(request: MapRequest): Flow<AirportLocation> {
        // ...
    }
}

上記の @MessageMapping メソッドは、「locate.radars.within」というルートを持つ Request-Stream インタラクションに応答します。次のメソッド引数を使用するオプションを備えた柔軟なメソッドシグネチャーをサポートしています。

メソッド引数 説明

@Payload

リクエストのペイロード。これは、Mono や Flux などの非同期タイプの具体的な値になります。

注意 : アノテーションの使用はオプションです。単純型ではなく、サポートされている他の引数のいずれでもないメソッド引数は、予期されるペイロードと見なされます。

RSocketRequester

リモートの終了をリクエストするためのリクエスター。

@DestinationVariable

マッピングパターンの変数に基づいてルートから抽出された値。@MessageMapping("find.radar.{id}")

@Header

MetadataExtractor で説明されている抽出のために登録されたメタデータ値。

@Headers Map<String, Object>

MetadataExtractor に従って、抽出のために登録されたすべてのメタデータ値。

戻り値は、レスポンスペイロードとして直列化される 1 つ以上のオブジェクトであると予想されます。これは、Mono または Flux のような非同期タイプ、具体的な値、または void または Mono<Void> などの値のない非同期タイプのいずれかです。

@MessageMapping メソッドがサポートする RSocket インタラクションタイプは、入力(つまり、@Payload 引数)と出力のカーディナリティから決定されます。カーディナリティは次のことを意味します。

基数 説明

1

明示的な値、または Mono<T> などの単一値の非同期タイプのいずれか。

Flux<T> などの複数値非同期タイプ。

0

入力の場合、これはメソッドに @Payload 引数がないことを意味します。

出力の場合、これは void または Mono<Void> などの値のない非同期タイプです。

以下の表は、すべての入力および出力カーディナリティーの組み合わせと、対応する相互作用タイプを示しています。

入力カーディナリティ 出力カーディナリティ インタラクションタイプ

0, 1

0

火と忘れ、リクエストとレスポンス

0, 1

1

リクエスト - レスポンス

0, 1

リクエストストリーム

0, 1、多数

リクエストチャネル

5.3.4. @ConnectMapping

@ConnectMapping は RSocket 接続の開始時に SETUP フレームを処理し、後続のメタデータは METADATA_PUSH フレーム、つまり io.rsocket.RSocket の metadataPush(Payload) を介して通知をプッシュします。

@ConnectMapping メソッドは、@MessageMapping と同じ引数をサポートしていますが、SETUP および METADATA_PUSH フレームからのメタデータとデータに基づいています。@ConnectMapping には、メタデータにルートを持つ特定の接続に処理を絞り込むパターンを設定できます。パターンが宣言されていない場合は、すべての接続が一致します。

@ConnectMapping メソッドはデータを返すことができず、戻り値として void または Mono<Void> を使用して宣言する必要があります。新しい接続に対して処理がエラーを返した場合、接続は拒否されます。接続のために RSocketRequester にリクエストを行うために、処理を遅らせてはなりません。詳細については、サーバーリクエスターを参照してください。

5.4. MetadataExtractor

応答者はメタデータを解釈する必要があります。複合メタデータ (GitHub) では、それぞれ独自の MIME タイプを使用して、個別にフォーマットされたメタデータ値(ルーティング、セキュリティ、トレースなど)を使用できます。アプリケーションには、サポートするメタデータ MIME タイプを構成する方法と、抽出された値にアクセスする方法が必要です。

MetadataExtractor は、直列化されたメタデータを取得し、デコードされた名前と値のペアを返す契約です。名前付きのヘッダーのように、たとえばアノテーション付きハンドラーメソッドの @Header を介してアクセスできます。

DefaultMetadataExtractor に Decoder インスタンスを与えて、メタデータをデコードできます。すぐに使用できる「message/x.rsocket.routing.v0」 (GitHub) の組み込みサポートがあり、String にデコードして「ルート」キーに保存します。その他の MIME タイプの場合は、Decoder を提供して、MIME タイプを次のように登録する必要があります。

Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")

複合メタデータは、独立したメタデータ値を組み合わせるのに適しています。ただし、リクエスターは複合メタデータをサポートしていないか、使用しないことを選択する場合があります。このため、DefaultMetadataExtractor は、デコードされた値を出力マップにマップするカスタムロジックを必要とする場合があります。JSON がメタデータに使用される例を次に示します。

Java
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
    MimeType.valueOf("application/vnd.myapp.metadata+json"),
    new ParameterizedTypeReference<Map<String,String>>() {},
    (jsonMap, outputMap) -> {
        outputMap.putAll(jsonMap);
    });
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
    outputMap.putAll(jsonMap)
}

MetadataExtractor から RSocketStrategies を構成する場合、RSocketStrategies.Builder に構成済みのデコーダーを使用して抽出プログラムを作成させ、コールバックを使用して登録を次のようにカスタマイズできます。

Java
RSocketStrategies strategies = RSocketStrategies.builder()
    .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        // ...
    })
    .build();
Kotlin
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
        .metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
            registry.metadataToExtract<Foo>(fooMimeType, "foo")
            // ...
        }
        .build()

6. リアクティブライブラリ

spring-webflux は reactor-core に依存し、それを内部的に使用して非同期ロジックを構成し、Reactive Streams サポートを提供します。通常、WebFlux API は Flux または Mono を返し(これらは内部で使用されるため)、入力として Reactive Streams Publisher 実装を寛容に受け入れます。Flux 対 Mono の使用は重要です。カーディナリティーを表現するのに役立つためです - たとえば、単一または複数の非同期値が予想されるかどうか、それは決定を行うために不可欠です(たとえば、HTTP メッセージをエンコードまたはデコードする場合)。

アノテーション付きコントローラーの場合、WebFlux はアプリケーションが選択したリアクティブライブラリに透過的に適応します。これは、ReactiveAdapterRegistry(Javadoc) の助けを借りて行われます。ReactiveAdapterRegistry(Javadoc) は、リアクティブライブラリおよびその他の非同期タイプのプラグ可能なサポートを提供します。レジストリには RxJava および CompletableFuture のサポートが組み込まれていますが、他のユーザーも登録できます。

関数 API(関数エンドポイントWebClient など)の場合、WebFlux API の一般規則が適用されます。戻り値として Flux および Mono、入力として Reactive Streams PublisherPublisher がカスタムまたは別のリアクティブライブラリから提供された場合、未知のセマンティクス(0..N)を持つストリームとしてのみ扱うことができます。ただし、セマンティクスがわかっている場合は、生の Publisher を渡す代わりに、Flux または Mono.from(Publisher) でラップすることができます。

例: Mono ではない Publisher を指定すると、Jackson JSON メッセージライターは複数の値を予期します。メディアタイプが無限ストリーム(application/json+stream など)を意味する場合、値は個別に書き込まれ、フラッシュされます。それ以外の場合、値はリストにバッファリングされ、JSON 配列としてレンダリングされます。

Unofficial Translation by spring.pleiades.io. See the original content.