ドキュメントのこのパートでは、Netty、Undertow、Servlet 3.1+ コンテナーなどのノンブロッキングサーバーで実行する Reactive Streams (英語) API 上に構築されたリアクティブスタック Web アプリケーションのサポートについて説明します。個々の章では、Spring WebFlux フレームワーク、リアクティブ WebClient、テストのサポート、およびリアクティブライブラリについて説明します。サーブレットスタック Web アプリケーションについては、Web MVC サーブレットスタックを参照してください。
1. Spring WebFlux
Spring Framework に含まれている元の Web フレームワークである Spring Web MVC は、サーブレット 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 つは、少ないスレッドで同時実行を処理し、より少ないハードウェアリソースで拡張するために、ノンブロッキング Web スタックの必要性です。Servlet 3.1 は、ノンブロッキング I/O 用の API を提供しました。ただし、これを使用すると、契約が同期(Filter、Servlet)またはブロッキング(getParameter、getPart)であるサーブレット 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 でも採用 (標準 Javadoc) )です。たとえば、データリポジトリ(パブリッシャー (英語) として機能)は、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 アプリケーションがある場合は、変更する必要はありません。命令型プログラミングは、コードを記述、理解し、デバッグする最も簡単な方法です。歴史的に、ほとんどのライブラリがブロックされているため、ライブラリの選択肢は最大限に広がります。
すでにノンブロッキング 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、またはその他) を返すことができます。 1 回の呼び出しあたりの待機時間または呼び出し間の相互依存性が大きいほど、メリットは大きくなります。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 は、サーブレット API なしで Undertow API を直接使用します。
1.1.6. パフォーマンス
パフォーマンスには多くの特性と意味があります。通常、リアクティブでノンブロッキングでは、アプリケーションの実行速度は向上しません。場合によっては(たとえば、WebClient を使用してリモート呼び出しを並列に実行する場合)全体として、ノンブロッキングの方法を実行するにはより多くの作業が必要であり、必要な処理時間をわずかに増加させる可能性があります。
リアクティブでノンブロッキングの主な期待される利点は、少数の固定スレッドと少ないメモリで拡張できることです。予測可能な方法でスケーリングされるため、負荷がかかった場合でもアプリケーションの回復力が高まります。ただし、これらの利点を観測するには、ある程度の遅延(低速で予測不可能なネットワーク I/O の混在を含む)が必要です。それが、リアクティブスタックの強みを示し始めた場所であり、違いは劇的です。
1.1.7. 同時実行モデル
Spring MVC と Spring WebFlux はどちらもアノテーション付きコントローラーをサポートしていますが、同時実行モデルと、ブロックとスレッドのデフォルトの前提には重要な違いがあります。
Spring MVC (および一般的なサーブレットアプリケーション) では、アプリケーションが現在のスレッドをブロックできることが想定されています (たとえば、リモート呼び出しの場合)。このため、サーブレットコンテナーは大きなスレッドプールを使用して、リクエスト処理中の潜在的なブロックを吸収します。
Spring WebFlux(および一般にノンブロッキングサーバー)では、アプリケーションはブロックされないと想定されています。ノンブロッキングサーバーは、小さな固定サイズのスレッドプール(イベントループワーカー)を使用してリクエストを処理します。
| 「スケーリングする」と「少ないスレッド」は矛盾しているように聞こえるかもしれませんが、現在のスレッドをブロックしない(および代わりにコールバックに依存する)ことは、吸収するブロッキング呼び出しがないため、余分なスレッドが必要ないことを意味します。 |
ブロッキングライブラリを使用する必要がある場合はどうなるでしょうか? 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戦略を使用していることを意味します。データアクセスライブラリおよびその他のサードパーティの依存関係も、独自のスレッドを作成して使用できます。
1.2. リアクティブコア
spring-web モジュールには、リアクティブ Web アプリケーションに対する次の基本的なサポートが含まれています。
サーバーリクエスト処理には、2 つのレベルのサポートがあります。
HttpHandler : Reactor Netty、Undertow、Tomcat、Jetty、任意の Servlet 3.1+ コンテナー用のアダプターとともに、ノンブロッキング I/O および Reactive Streams バックプレッシャを使用した HTTP リクエスト処理の基本契約。
WebHandlerAPI : リクエスト処理用のわずかに高いレベルの汎用 Web API。その上に、アノテーション付きコントローラーや関数エンドポイントなどの具体的なプログラミングモデルが構築されます。
クライアント側には、ノンブロッキング I/O と Reactive Streams バックプレッシャーを使用して HTTP リクエストを実行するための基本的な
ClientHttpConnector契約と、Reactor Netty [GitHub] (英語) 、リアクティブ Jetty HttpClient [GitHub] (英語) および Apache HttpComponents (英語) のアダプターがあります。アプリケーションで使用される上位レベルの WebClient は、この基本契約に基づいて構築されています。クライアントおよびサーバーの場合、HTTP リクエストおよびレスポンスコンテンツのシリアライズおよびデシリアライズ用のコーデック。
1.2.1. HttpHandler
HttpHandler (Javadoc) は、リクエストとレスポンスを処理する単一のメソッドを持つ単純な契約です。これは意図的に最小限に抑えられており、その主な唯一の目的は、さまざまな HTTP サーバー API を最小限に抽象化することです。
次の表に、サポートされているサーバー API を示します。
| サーバー名 | 使用されるサーバー API | Reactive Streams サポート |
|---|---|---|
Netty | Netty API | |
Undertow | Undertow API | spring-web: Undertow から 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
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()Undertow
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()Tomcat
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)
val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()Jetty
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()Servlet 3.1+ コンテナー
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> |
| 0..N |
|
<any> |
| 0..N | フィルターチェーンの残りとターゲット |
|
| 1 | リクエストのハンドラー。 |
|
| 0..1 |
|
|
| 0..1 | フォームデータとマルチパートデータを解析するための |
|
| 0..1 |
|
|
| 0..1 | 転送された型ヘッダーを処理するには、抽出して削除するか、削除するだけです。デフォルトでは使用されません。 |
フォームデータ
ServerWebExchange は、フォームデータにアクセスするための次のメソッドを公開します。
Mono<MultiValueMap<String, String>> getFormData();suspend fun getFormData(): MultiValueMap<String, String>DefaultServerWebExchange は、構成された HttpMessageReader を使用して、フォームデータ(application/x-www-form-urlencoded)を解析して MultiValueMap にします。デフォルトでは、FormHttpMessageReader は ServerCodecConfigurer Bean が使用するように構成されています(Web ハンドラー API を参照)。
マルチパートデータ
ServerWebExchange は、マルチパートデータにアクセスするための次のメソッドを公開します。
Mono<MultiValueMap<String, Part>> getMultipartData();suspend fun getMultipartData(): MultiValueMap<String, Part>DefaultServerWebExchange は、構成された HttpMessageReader<MultiValueMap<String, Part>> を使用して、multipart/form-data コンテンツを MultiValueMap に解析します。デフォルトでは、これは DefaultPartHttpMessageReader であり、サードパーティの依存関係はありません。あるいは、Synchronoss NIO マルチパート [GitHub] (英語) ライブラリに基づく SynchronossPartHttpMessageReader を使用することもできます。どちらも ServerCodecConfigurer Bean を介して構成されます(Web ハンドラー API を参照)。
ストリーミング形式でマルチパートデータを解析するには、代わりに HttpMessageReader<Part> から返された Flux<Part> を使用できます。例: アノテーション付きコントローラーでは、@RequestPart の使用は Map -like が名前で個々のパーツにアクセスすることを意味するため、マルチパーツデータを完全に解析する必要があります。対照的に、@RequestBody を使用して、MultiValueMap に収集することなく、コンテンツを Flux<Part> にデコードできます。
Forwarded ヘッダー
リクエストがプロキシ(ロードバランサなど)を通過するときに、ホスト、ポート、スキームが変更される場合があります。そのため、クライアントの観点からは、正しいホスト、ポート、スキームを指すリンクを作成することが困難になります。
RFC 7239 [IETF] (英語) は、プロキシが元のリクエストに関する情報を提供するために使用できる Forwarded HTTP ヘッダーを定義します。X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-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 および CORS WebFilter のセクションを参照してください。
1.2.4. 例外
WebHandler API では、WebExceptionHandler を使用して、WebFilter インスタンスのチェーンおよびターゲット WebHandler からの例外を処理できます。WebFlux 構成を使用する場合、WebExceptionHandler の登録は、Spring Bean として宣言し、(オプションで)Bean 宣言で @Order を使用するか、Ordered を実装することによって優先順位を表すのと同じくらい簡単です。
次の表は、利用可能な WebExceptionHandler 実装を説明しています。
| 例外ハンドラー | 説明 |
|---|---|
| 例外の HTTP ステータスコードへのレスポンスを設定することにより、型 |
|
このハンドラーは 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) は、さまざまなバイトバッファ表現(NettyByteBuf、java.nio.ByteBufferなど)を抽象化し、すべてのコーデックが機能するものです。このトピックの詳細については、「Spring コア」セクションのデータバッファとコーデックを参照してください。
spring-core モジュールは、byte[]、ByteBuffer、DataBuffer、Resource、String エンコーダーおよびデコーダーの実装を提供します。spring-web モジュールは、Jackson JSON、Jackson Smile、JAXB2、Protocol Buffers、その他のエンコーダーとデコーダー、フォームデータ、マルチパートコンテンツ、サーバー送信イベントなどの 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 配列、または NDJSON、JSON 行、JSON テキストシーケンスなどの行区切りの JSON [Wikipedia] (英語) 形式にすることができます。
Jackson2Encoder は次のように機能します。
単一の値のパブリッシャー(例:
Mono)の場合、単にObjectMapperを介して直列化します。application/jsonを使用する複数値パブリッシャーの場合、デフォルトではFlux#collectToList()を使用して値を収集し、結果のコレクションを直列化します。application/x-ndjsonやapplication/stream+x-jackson-smileなどのストリーミングメディア型を持つ複数値のパブリッシャーの場合、行区切りの JSON [Wikipedia] (英語) 形式を使用して、各値を個別にエンコード、書き込み、フラッシュします。他のストリーミングメディア型は、エンコーダに登録される場合があります。SSE の場合、
Jackson2Encoderはイベントごとに呼び出され、出力はフラッシュされ、遅延なく配信されます。
デフォルトでは、 |
フォームデータ
FormHttpMessageReader および FormHttpMessageWriter は、application/x-www-form-urlencoded コンテンツのデコードおよびエンコードをサポートします。
フォームコンテンツに複数の場所から頻繁にアクセスする必要があるサーバー側では、ServerWebExchange は、FormHttpMessageReader を介してコンテンツを解析し、繰り返しアクセスするために結果をキャッシュする専用の getFormData() メソッドを提供します。WebHandler API セクションのフォームデータを参照してください。
getFormData() を使用すると、元の生のコンテンツをリクエスト本文から読み取ることはできなくなります。このため、キャッシュされたフォームデータへのアクセスと生のリクエスト本文からの読み取りでは、アプリケーションは ServerWebExchange を一貫して通過することが期待されます。
マルチパート
MultipartHttpMessageReader および MultipartHttpMessageWriter は、"multipart/form-data" コンテンツのデコードとエンコードをサポートします。次に、MultipartHttpMessageReader は別の HttpMessageReader に委譲して、Flux<Part> への実際の解析を行い、パーツを MultiValueMap に収集します。デフォルトでは DefaultPartHttpMessageReader が使用されますが、これは ServerCodecConfigurer を介して変更できます。DefaultPartHttpMessageReader の詳細については、DefaultPartHttpMessageReader の javadoc を参照してください。
マルチパートフォームコンテンツに複数の場所からアクセスする必要があるサーバー側では、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-stream、application/x-ndjson)にストリーミングする場合、切断されたクライアントを後でではなく早く確実に検出するために、定期的にデータを送信することが重要です。このような送信は、コメントのみの空の 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 ロギングは、機密情報を記録できます。これが、フォームパラメーターとヘッダーがデフォルトでマスクされる理由であり、それらのロギングを完全に明示的に有効にする必要があります。
次の例は、サーバー側のリクエストに対してこれを行う方法を示しています。
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true);
}
}@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
configurer.defaultCodecs().enableLoggingRequestDetails(true)
}
}次の例は、クライアント側のリクエストに対してこれを行う方法を示しています。
Consumer<ClientCodecConfigurer> consumer = configurer ->
configurer.defaultCodecs().enableLoggingRequestDetails(true);
WebClient webClient = WebClient.builder()
.exchangeStrategies(strategies -> strategies.codecs(consumer))
.build();val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }
val webClient = WebClient.builder()
.exchangeStrategies({ strategies -> strategies.codecs(consumer) })
.build()アペンダー
SLF4J や Log4J2 などのロギングライブラリは、ブロッキングを回避する非同期ロガーを提供します。これらには、ロギング用にキューに入れることができなかったメッセージをドロップする可能性があるなどの独自の欠点がありますが、これらは、リアクティブなノンブロッキングアプリケーションで現在使用できる最良のオプションです。
カスタムコーデック
アプリケーションは、追加のメディア型をサポートするカスタムコーデック、またはデフォルトコーデックでサポートされていない特定の動作を登録できます。
開発者によって表された一部の構成オプションは、デフォルトのコーデックに適用されます。カスタムコーデックは、バッファリング制限の強制や機密データのログ記録など、これらの設定に合わせて調整する機会を得たい場合があります。
次の例は、クライアント側のリクエストに対してこれを行う方法を示しています。
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()1.3. DispatcherHandler
Spring WebFlux は、Spring MVC と同様に、フロントコントローラーパターンを中心に設計されており、主要な WebHandler である DispatcherHandler がリクエスト処理の共有アルゴリズムを提供し、実際の作業は構成可能なデリゲートコンポーネントによって実行されます。このモデルは柔軟で、多様なワークフローをサポートします。
DispatcherHandler は、Spring 構成から必要なデリゲートコンポーネントを検出します。また、Spring Bean 自体になるように設計されており、実行されるコンテキストにアクセスするために ApplicationContextAware を実装しています。DispatcherHandler が webHandler という Bean 名で宣言されている場合、WebHandler API に従って、リクエスト処理チェーンをまとめる WebHttpHandlerBuilder (Javadoc) によって検出されます。
WebFlux アプリケーションの Spring 構成には通常、次のものが含まれます。
DispatcherHandler、Bean 名はwebHandlerWebFilterおよびWebExceptionHandlerBeanその他
次の例が示すように、処理はチェーンを構築するために WebHttpHandlerBuilder に与えられます。
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();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 型 | 説明 |
|---|---|
| リクエストをハンドラーにマップします。マッピングはいくつかの条件に基づいており、その詳細は 主な |
| ハンドラーが実際に呼び出される方法に関係なく、 |
| ハンドラー呼び出しからの結果を処理し、レスポンスを確定します。結果処理を参照してください。 |
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 構成で宣言されています。
| 結果ハンドラーの型 | 戻り値 | デフォルトの順序 |
|---|---|---|
|
|
0 |
|
|
0 |
|
| 100 |
|
See also View Resolution. |
|
1.3.5. 例外
HandlerAdapter から返された HandlerResult は、ハンドラー固有のメカニズムに基づいてエラー処理用の関数を公開できます。このエラー関数は次の場合に呼び出されます:
ハンドラー(たとえば、
@Controller)呼び出しは失敗します。HandlerResultHandlerを介したハンドラー戻り値の処理は失敗します。
エラー関数は、ハンドラーから返されるリアクティブ型がデータ項目を生成する前にエラーシグナルが発生する限り、レスポンスを(たとえば、エラー状況に)変更できます。
これが、@Controller クラスの @ExceptionHandler メソッドがサポートされる方法です。対照的に、Spring MVC での同じサポートは HandlerExceptionResolver 上に構築されています。通常、これは重要ではありません。ただし、WebFlux では、ハンドラーが選択される前に発生した例外を @ControllerAdvice を使用して処理できないことに注意してください。
1.3.6. ビューリゾルバー
ビューリゾルバーにより、特定のビューテクノロジに縛られることなく、HTML テンプレートとモデルを使用してブラウザーにレンダリングできます。Spring WebFlux では、ViewResolver インスタンスを使用してストリング(論理ビュー名を表す)を View インスタンスにマップする専用 HandlerResultHandler を介して、ビューリゾルバーがサポートされています。次に、View を使用してレスポンスをレンダリングします。
ハンドリング
ViewResolutionResultHandler に渡される HandlerResult には、ハンドラーからの戻り値と、リクエスト処理中に追加された属性を含むモデルが含まれます。戻り値は次のいずれかとして処理されます。
String,CharSequence: 構成されたViewResolver実装のリストを通じてViewに解決される論理ビュー名。void: リクエストパスに基づいて、先頭と末尾のスラッシュを除いたデフォルトのビュー名を選択し、Viewに解決します。ビュー名が指定されなかった場合(モデル属性が返された場合など)、または非同期の戻り値(たとえば、空のMonoが完了した場合)でも同じことが起こります。レンダリング (Javadoc) : ビュー解決シナリオの API。コード補完を使用して IDE のオプションを調べましょう。
Model,Map: リクエストのモデルに追加される追加のモデル属性。他: その他の戻り値(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 コンポーネントは、アノテーションを使用して、リクエストマッピング、リクエスト入力、例外処理などを表現します。アノテーション付きコントローラーには柔軟なメソッドシグネチャーがあり、基本クラスを継承したり、特定のインターフェースを実装したりする必要はありません。
次のリストは、基本的な例を示しています。
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}@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 構成にコンポーネントスキャンを追加できます。
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {
// ...
}| 1 | org.example.web パッケージをスキャンします。 |
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {
// ...
}| 1 | org.example.web パッケージをスキャンします。 |
@RestController は、それ自体が @Controller および @ResponseBody でメタアノテーションが付けられた合成アノテーションであり、すべてのメソッドが型レベルの @ResponseBody アノテーションを継承するコントローラーを示します。ビューリゾルバーと HTML テンプレートを使用したレンダリングではなく、レスポンス本文に直接書き込みます。
1.4.2. リクエストマッピング
@RequestMapping アノテーションは、リクエストをコントローラーメソッドにマップするために使用されます。URL、HTTP メソッド、リクエストパラメーター、ヘッダー、メディア型で一致するさまざまな属性があります。クラスレベルで使用して共有マッピングを表現したり、メソッドレベルで使用して特定のエンドポイントマッピングに絞り込んだりできます。
@RequestMapping の HTTP メソッド固有のショートカットバリアントもあります。
@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping
ほとんどのコントローラーメソッドは、デフォルトですべての HTTP メソッドに一致する @RequestMapping を使用するのではなく、特定の HTTP メソッドにマップされる必要があるため、上記のアノテーションはカスタムアノテーションです。同時に、共有マッピングを表現するには、クラスレベルで @RequestMapping が必要です。
次の例では、型およびメソッドレベルのマッピングを使用しています。
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}@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 文字に一致 |
|
| パスセグメント内の 0 個以上の文字に一致します |
|
| パスの終わりまで 0 個以上のパスセグメントに一致します |
|
| パスセグメントを照合し、"name" という名前の変数としてキャプチャーします |
|
| 正規表現 |
|
| パスの最後まで 0 個以上のパスセグメントに一致し、"path" という名前の変数としてキャプチャーします |
|
次の例に示すように、キャプチャーされた URI 変数には @PathVariable を使用してアクセスできます。
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}次の例に示すように、クラスおよびメソッドレベルで URI 変数を宣言できます。
@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 マッピング。 |
@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 が発生します。単純型(int、long、Date など)はデフォルトでサポートされており、他のデータ型のサポートを登録できます。型変換および DataBinder を参照してください。
URI 変数には明示的に名前を付けることができます(たとえば、@PathVariable("customId"))。ただし、名前が同じで、デバッグ情報または Java 8 の -parameters コンパイラーフラグを使用してコードをコンパイルする場合は、詳細を省略できます。
構文 {*varName} は、0 個以上の残りのパスセグメントに一致する URI 変数を宣言します。たとえば、/resources/{*path} は /resources/ のすべてのファイルと一致し、"path" 変数は /resources の完全なパスをキャプチャーします。
構文 {varName:regex} は、構文 {varName:regex} を持つ正規表現で URI 変数を宣言します。例: /spring-web-3.0.5.jar の URL を指定すると、次のメソッドは名前、バージョン、ファイル拡張子を抽出します。
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
// ...
}URI パスパターンには、起動時にローカル、システム、環境、その他のプロパティソースに対して PropertySourcesPlaceholderConfigurer を介して解決される埋め込み ${…} プレースホルダーを含めることもできます。これを使用して、たとえば、外部構成に基づいてベース URL をパラメーター化できます。
Spring WebFlux は、URI パス一致サポートのために PathPattern および PathPatternParser を使用します。両方のクラスは spring-web にあり、実行時に多数の URI パスパターンが一致する Web アプリケーションの HTTP URL パスで使用するために特別に設計されています。 |
Spring WebFlux はサフィックスパターンマッチングをサポートしていません — Spring MVC とは異なり、/person などのマッピングは /person.* にも一致します。URL ベースのコンテンツネゴシエーションでは、必要に応じて、クエリパラメーターを使用することをお勧めします。クエリパラメーターは、より単純で、より明示的で、URL パスベースのエクスプロイトに対して脆弱ではありません。
パターン比較
複数のパターンが URL に一致する場合、比較して最適な一致を見つける必要があります。これは、より具体的なパターンを探す PathPattern.SPECIFICITY_COMPARATOR で行われます。
すべてのパターンについて、URI 変数とワイルドカードの数に基づいてスコアが計算されます。URI 変数のスコアはワイルドカードよりも低くなります。合計スコアの低いパターンが優先されます。2 つのパターンのスコアが同じ場合、長い方が選択されます。
キャッチオールパターン(たとえば、**、{*varName})はスコアリングから除外され、代わりに常に最後にソートされます。2 つのパターンが両方ともキャッチオールである場合、長い方が選択されます。
消費可能なメディア型
次の例に示すように、リクエストの Content-Type に基づいてリクエストマッピングを絞り込むことができます。
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
// ...
} 消費属性は否定表現もサポートしています。たとえば、!text/plain は text/plain 以外のコンテンツ型を意味します。
クラスレベルで共有 consumes 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの consumes 属性は、クラスレベルの宣言を継承するのではなくオーバーライドします。
MediaType は、APPLICATION_JSON_VALUE や APPLICATION_XML_VALUE など、一般的に使用されるメディア型に定数を提供します。 |
生産可能なメディア型
次の例に示すように、Accept リクエストヘッダーとコントローラーメソッドが生成するコンテンツ型のリストに基づいて、リクエストマッピングを絞り込むことができます。
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
} メディア型は文字セットを指定できます。否定表現がサポートされています。たとえば、!text/plain は text/plain 以外のコンテンツ型を意味します。
クラスレベルで共有 produces 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの produces 属性は、クラスレベルの宣言を継承するのではなくオーバーライドします。
MediaType は、一般的に使用されるメディア型に定数を提供します。APPLICATION_JSON_VALUE、APPLICATION_XML_VALUE |
パラメーターとヘッダー
クエリパラメーターの条件に基づいてリクエストマッピングを絞り込むことができます。クエリパラメーターの存在(myParam)、その不在(!myParam)、特定の値(myParam=myValue)をテストできます。次の例では、値を持つパラメーターをテストします。
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}| 1 | myParam が myValue と等しいことを確認してください。 |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}| 1 | myParam が myValue と等しいことを確認してください。 |
次の例に示すように、リクエストヘッダー条件で同じものを使用することもできます。
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}| 1 | myHeader が myValue と等しいことを確認してください。 |
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}| 1 | myHeader が 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 での同じハンドラーの異なるインスタンスなどの高度なケースに使用できます。次の例は、その方法を示しています。
@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 | 登録を追加します。 |
@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 と同等です。
| コントローラーメソッドの引数 | 説明 |
|---|---|
| 完全な |
| HTTP リクエストまたはレスポンスへのアクセス。 |
| セッションへのアクセス。これは、属性が追加されない限り、新しいセッションの開始を強制しません。リアクティブ型をサポートします。 |
| 現在認証されているユーザー — 既知の場合、特定の |
| リクエストの HTTP メソッド。 |
| 利用可能な最も具体的な |
|
|
| URI テンプレート変数へのアクセス用。URI パターンを参照してください。 |
| URI パスセグメントの名前と値のペアへのアクセス用。マトリックス変数を参照してください。 |
| クエリパラメーターへのアクセス用。パラメーター値は、宣言されたメソッド引数の型に変換されます。
|
| リクエストヘッダーへのアクセス用。ヘッダー値は、宣言されたメソッド引数型に変換されます。 |
| クッキーへのアクセス用。Cookie 値は、宣言されたメソッド引数型に変換されます。 |
| HTTP リクエスト本文へのアクセス用。本文コンテンツは、 |
| リクエストヘッダーと本文へのアクセス用。本体は |
|
|
|
For access to the model that is used in HTML controllers and is exposed to templates as part of view rendering. |
| データバインディングと検証が適用されたモデル内の既存の属性(存在しない場合はインスタンス化)へのアクセス用。
|
| コマンドオブジェクト、つまり |
| フォーム処理の完了をマークするために、クラスレベルの |
| 現在のリクエストのホスト、ポート、スキーム、コンテキストパスに関連する URL を準備するため。URI リンクを参照してください。 |
| 任意のセッション属性へのアクセス— クラスレベルの |
| リクエスト属性へのアクセス用。詳細については、 |
その他の引数 | メソッドの引数が上記のいずれにも一致しない場合、デフォルトでは、BeanUtils#isSimpleProperty (Javadoc) によって決定される単純型の場合は |
戻り値
次の表に、サポートされているコントローラーメソッドの戻り値を示します。このよう Reactor、RxJava、などのライブラリからの反応型のことを注意または他の一般的にすべての戻り値のためにサポートされています。
| コントローラーメソッドの戻り値 | 説明 |
|---|---|
| 戻り値は、 |
| 戻り値は、HTTP ヘッダーを含む完全なレスポンスを指定し、本文は |
| ヘッダーを含み、本文を含まないレスポンスを返すため。 |
|
|
| 暗黙的なモデルと一緒にレンダリングするために使用する |
| 暗黙的なモデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。 |
| モデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。
|
| モデルおよびビューのレンダリングシナリオ用の API。 |
|
上記のいずれにも当てはまらない場合、 |
|
Emit server-sent events. The |
Other return values |
If a return value remains unresolved in any other way, it is treated as a model attribute, unless it is a simple type as determined by BeanUtils#isSimpleProperty (Javadoc) , in which case it remains unresolved. |
Type Conversion
Some annotated controller method arguments that represent String-based request input (for example,
@RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, and @CookieValue)
can require type conversion if the argument is declared as something other than String.
For such cases, type conversion is automatically applied based on the configured converters.
By default, simple types (such as int, long, Date, and others) are supported. Type conversion
can be customized through a WebDataBinder (see DataBinder) or by registering
Formatters with the FormattingConversionService (see Spring Field Formatting).
A practical issue in type conversion is the treatment of an empty String source value.
Such a value is treated as missing if it becomes null as a result of type conversion.
This can be the case for Long, UUID, and other target types. If you want to allow null
to be injected, either use the required flag on the argument annotation, or declare the
argument as @Nullable.
Matrix Variables
RFC 3986 [IETF] (英語) discusses name-value pairs in path segments. In Spring WebFlux, we refer to those as “matrix variables” based on an “old post” [W3C] (英語) by Tim Berners-Lee, but they can be also be referred to as URI path parameters.
Matrix variables can appear in any path segment, with each variable separated by a semicolon and
multiple values separated by commas — for example, "/cars;color=red,green;year=2012". Multiple
values can also be specified through repeated variable names — for example,
"color=red;color=green;color=blue".
Unlike Spring MVC, in WebFlux, the presence or absence of matrix variables in a URL does not affect request mappings. In other words, you are not required to use a URI variable to mask variable content. That said, if you want to access matrix variables from a controller method, you need to add a URI variable to the path segment where matrix variables are expected. The following example shows how to do so:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
Given that all path segments can contain matrix variables, you may sometimes need to disambiguate which path variable the matrix variable is expected to be in, as the following example shows:
// 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
}
@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
}
You can define a matrix variable may be defined as optional and specify a default value as the following example shows:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
To get all matrix variables, use a MultiValueMap, as the following example shows:
// 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]
}
// 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
You can use the @RequestParam annotation to bind query parameters to a method argument in a
controller. The following code snippet shows the usage:
@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 | Using @RequestParam. |
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 | Using @RequestParam. |
The Servlet API “request parameter” concept conflates query parameters, form
data, and multiparts into one. However, in WebFlux, each is accessed individually through
ServerWebExchange. While @RequestParam binds to query parameters only, you can use
data binding to apply query parameters, form data, and multiparts to a
command object.
|
Method parameters that use the @RequestParam annotation are required by default, but
you can specify that a method parameter is optional by setting the required flag of a @RequestParam
to false or by declaring the argument with a java.util.Optional
wrapper.
Type conversion is applied automatically if the target method parameter type is not
String. See Type Conversion.
When a @RequestParam annotation is declared on a Map<String, String> or
MultiValueMap<String, String> argument, the map is populated with all query parameters.
Note that use of @RequestParam is optional — for example, to set its attributes. By
default, any argument that is a simple value type (as determined by
BeanUtils#isSimpleProperty (Javadoc) )
and is not resolved by any other argument resolver is treated as if it were annotated
with @RequestParam.
@RequestHeader
You can use the @RequestHeader annotation to bind a request header to a method argument in a
controller.
The following example shows a request with headers:
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
The following example gets the value of the Accept-Encoding and Keep-Alive headers:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
| 1 | Get the value of the Accept-Encoding header. |
| 2 | Get the value of the Keep-Alive header. |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
| 1 | Get the value of the Accept-Encoding header. |
| 2 | Get the value of the Keep-Alive header. |
Type conversion is applied automatically if the target method parameter type is not
String. See Type Conversion.
When a @RequestHeader annotation is used on a Map<String, String>,
MultiValueMap<String, String>, or HttpHeaders argument, the map is populated
with all header values.
Built-in support is available for converting a comma-separated string into an
array or collection of strings or other types known to the type conversion system. For
example, a method parameter annotated with @RequestHeader("Accept") may be of type
String but also of String[] or List<String>.
|
@CookieValue
You can use the @CookieValue annotation to bind the value of an HTTP cookie to a method argument
in a controller.
The following example shows a request with a cookie:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
The following code sample demonstrates how to get the cookie value:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
| 1 | Get the cookie value. |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
| 1 | Get the cookie value. |
Type conversion is applied automatically if the target method parameter type is not
String. See Type Conversion.
@ModelAttribute
You can use the @ModelAttribute annotation on a method argument to access an attribute from the
model or have it instantiated if not present. The model attribute is also overlaid with
the values of query parameters and form fields whose names match to field names. This is
referred to as data binding, and it saves you from having to deal with parsing and
converting individual query parameters and form fields. The following example binds an instance of Pet:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
| 1 | Bind an instance of Pet. |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
| 1 | Bind an instance of Pet. |
The Pet instance in the preceding example is resolved as follows:
-
From the model if already added through
Model. -
From the HTTP session through
@SessionAttributes. -
From the invocation of a default constructor.
-
From the invocation of a “primary constructor” with arguments that match query parameters or form fields. Argument names are determined through JavaBeans
@ConstructorPropertiesor through runtime-retained parameter names in the bytecode.
After the model attribute instance is obtained, data binding is applied. The
WebExchangeDataBinder class matches names of query parameters and form fields to field
names on the target Object. Matching fields are populated after type conversion is applied
where necessary. For more on data binding (and validation), see
Validation. For more on customizing data binding, see
DataBinder.
Data binding can result in errors. By default, a WebExchangeBindException is raised, but,
to check for such errors in the controller method, you can add a BindingResult argument
immediately next to the @ModelAttribute, as the following example shows:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | Adding a BindingResult. |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
| 1 | Adding a BindingResult. |
You can automatically apply validation after data binding by adding the
javax.validation.Valid annotation or Spring’s @Validated annotation (see also
Bean Validation and
Spring validation). The following example uses the @Valid annotation:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | Using @Valid on a model attribute argument. |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
| 1 | Using @Valid on a model attribute argument. |
Spring WebFlux, unlike Spring MVC, supports reactive types in the model — for example,
Mono<Account> or io.reactivex.Single<Account>. You can declare a @ModelAttribute argument
with or without a reactive type wrapper, and it will be resolved accordingly,
to the actual value if necessary. However, note that, to use a BindingResult
argument, you must declare the @ModelAttribute argument before it without a reactive
type wrapper, as shown earlier. Alternatively, you can handle any errors through the
reactive type, as the following example shows:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
return petMono
.flatMap(pet -> {
// ...
})
.onErrorResume(ex -> {
// ...
});
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
return petMono
.flatMap { pet ->
// ...
}
.onErrorResume{ ex ->
// ...
}
}
Note that use of @ModelAttribute is optional — for example, to set its attributes.
By default, any argument that is not a simple value type (as determined by
BeanUtils#isSimpleProperty (Javadoc) )
and is not resolved by any other argument resolver is treated as if it were annotated
with @ModelAttribute.
@SessionAttributes
@SessionAttributes is used to store model attributes in the WebSession between
requests. It is a type-level annotation that declares session attributes used by a
specific controller. This typically lists the names of model attributes or types of
model attributes that should be transparently stored in the session for subsequent
requests to access.
Consider the following example:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
| 1 | Using the @SessionAttributes annotation. |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
| 1 | Using the @SessionAttributes annotation. |
On the first request, when a model attribute with the name, pet, is added to the model,
it is automatically promoted to and saved in the WebSession. It remains there until
another controller method uses a SessionStatus method argument to clear the storage,
as the following example shows:
@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 | Using the @SessionAttributes annotation. |
| 2 | Using a SessionStatus variable. |
@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 | Using the @SessionAttributes annotation. |
| 2 | Using a SessionStatus variable. |
@SessionAttribute
If you need access to pre-existing session attributes that are managed globally
(that is, outside the controller — for example, by a filter) and may or may not be present,
you can use the @SessionAttribute annotation on a method parameter, as the following example shows:
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
| 1 | Using @SessionAttribute. |
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
| 1 | Using @SessionAttribute. |
For use cases that require adding or removing session attributes, consider injecting
WebSession into the controller method.
For temporary storage of model attributes in the session as part of a controller
workflow, consider using SessionAttributes, as described in
@SessionAttributes.
@RequestAttribute
Similarly to @SessionAttribute, you can use the @RequestAttribute annotation to
access pre-existing request attributes created earlier (for example, by a WebFilter),
as the following example shows:
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
| 1 | Using @RequestAttribute. |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
| 1 | Using @RequestAttribute. |
Multipart Content
As explained in Multipart Data, ServerWebExchange provides access to multipart
content. The best way to handle a file upload form (for example, from a browser) in a controller
is through data binding to a command object,
as the following example shows:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
class MyForm(
val name: String,
val file: MultipartFile)
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
// ...
}
}
You can also submit multipart requests from non-browser clients in a RESTful service scenario. The following example uses a file along with 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 ...
You can access individual parts with @RequestPart, as the following example shows:
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file) { (2)
// ...
}
| 1 | Using @RequestPart to get the metadata. |
| 2 | Using @RequestPart to get the file. |
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
}
| 1 | Using @RequestPart to get the metadata. |
| 2 | Using @RequestPart to get the file. |
To deserialize the raw part content (for example, to JSON — similar to @RequestBody),
you can declare a concrete target Object, instead of Part, as the following example shows:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
| 1 | Using @RequestPart to get the metadata. |
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
| 1 | Using @RequestPart to get the metadata. |
You can use @RequestPart in combination with javax.validation.Valid or Spring’s
@Validated annotation, which causes Standard Bean Validation to be applied. Validation
errors lead to a WebExchangeBindException that results in a 400 (BAD_REQUEST) response.
The exception contains a BindingResult with the error details and can also be handled
in the controller method by declaring the argument with an async wrapper and then using
error related operators:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
// use one of the onError* operators...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}
To access all multipart data as a MultiValueMap, you can use @RequestBody,
as the following example shows:
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
| 1 | Using @RequestBody. |
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
| 1 | Using @RequestBody. |
To access multipart data sequentially, in streaming fashion, you can use @RequestBody with
Flux<Part> (or Flow<Part> in Kotlin) instead, as the following example shows:
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
| 1 | Using @RequestBody. |
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
// ...
}
| 1 | Using @RequestBody. |
@RequestBody
You can use the @RequestBody annotation to have the request body read and deserialized into an
Object through an HttpMessageReader.
The following example uses a @RequestBody argument:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
Unlike Spring MVC, in WebFlux, the @RequestBody method argument supports reactive types
and fully non-blocking reading and (client-to-server) streaming.
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
You can use the HTTP message codecs option of the WebFlux Config to configure or customize message readers.
You can use @RequestBody in combination with javax.validation.Valid or Spring’s
@Validated annotation, which causes Standard Bean Validation to be applied. Validation
errors cause a WebExchangeBindException, which results in a 400 (BAD_REQUEST) response.
The exception contains a BindingResult with error details and can be handled in the
controller method by declaring the argument with an async wrapper and then using error
related operators:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
// use one of the onError* operators...
}
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
// ...
}
HttpEntity
HttpEntity is more or less identical to using @RequestBody but is based on a
container object that exposes request headers and the body. The following example uses an
HttpEntity:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
You can use the @ResponseBody annotation on a method to have the return serialized
to the response body through an HttpMessageWriter. The following
example shows how to do so:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody is also supported at the class level, in which case it is inherited by
all controller methods. This is the effect of @RestController, which is nothing more
than a meta-annotation marked with @Controller and @ResponseBody.
@ResponseBody supports reactive types, which means you can return Reactor or RxJava
types and have the asynchronous values they produce rendered to the response.
For additional details, see Streaming and
JSON rendering.
You can combine @ResponseBody methods with JSON serialization views.
See Jackson JSON for details.
You can use the HTTP message codecs option of the WebFlux Config to configure or customize message writing.
ResponseEntity
ResponseEntity is like @ResponseBody but with status and headers. For example:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux supports using a single value reactive type to
produce the ResponseEntity asynchronously, and/or single and multi-value reactive types
for the body. This allows a variety of async responses with ResponseEntity as follows:
-
ResponseEntity<Mono<T>>orResponseEntity<Flux<T>>make the response status and headers known immediately while the body is provided asynchronously at a later point. UseMonoif the body consists of 0..1 values orFluxif it can produce multiple values. -
Mono<ResponseEntity<T>>provides all three — response status, headers, and body, asynchronously at a later point. This allows the response status and headers to vary depending on the outcome of asynchronous request handling. -
Mono<ResponseEntity<Mono<T>>>orMono<ResponseEntity<Flux<T>>>are yet another possible, albeit less common alternative. They provide the response status and headers asynchronously first and then the response body, also asynchronously, second.
Jackson JSON
Spring offers support for the Jackson JSON library.
JSON Views
Spring WebFlux provides built-in support for
Jackson’s Serialization Views (英語) ,
which allows rendering only a subset of all fields in an Object. To use it with
@ResponseBody or ResponseEntity controller methods, you can use Jackson’s
@JsonView annotation to activate a serialization view class, as the following example shows:
@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;
}
}
@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 allows an array of view classes but you can only specify only one per
controller method. Use a composite interface if you need to activate multiple views.
|
1.4.4. Model
You can use the @ModelAttribute annotation:
-
On a method argument in
@RequestMappingmethods to create or access an Object from the model and to bind it to the request through aWebDataBinder. -
As a method-level annotation in
@Controlleror@ControllerAdviceclasses, helping to initialize the model prior to any@RequestMappingmethod invocation. -
On a
@RequestMappingmethod to mark its return value as a model attribute.
This section discusses @ModelAttribute methods, or the second item from the preceding list.
A controller can have any number of @ModelAttribute methods. All such methods are
invoked before @RequestMapping methods in the same controller. A @ModelAttribute
method can also be shared across controllers through @ControllerAdvice. See the section on
Controller Advice for more details.
@ModelAttribute methods have flexible method signatures. They support many of the same
arguments as @RequestMapping methods (except for @ModelAttribute itself and anything
related to the request body).
The following example uses a @ModelAttribute method:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
The following example adds one attribute only:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
When a name is not explicitly specified, a default name is chosen based on the type,
as explained in the javadoc for Conventions (Javadoc) .
You can always assign an explicit name by using the overloaded addAttribute method or
through the name attribute on @ModelAttribute (for a return value).
|
Spring WebFlux, unlike Spring MVC, explicitly supports reactive types in the model
(for example, Mono<Account> or io.reactivex.Single<Account>). Such asynchronous model
attributes can be transparently resolved (and the model updated) to their actual values
at the time of @RequestMapping invocation, provided a @ModelAttribute argument is
declared without a wrapper, as the following example shows:
@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) {
// ...
}
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 {
// ...
}
In addition, any model attributes that have a reactive type wrapper are resolved to their actual values (and the model updated) just prior to view rendering.
You can also use @ModelAttribute as a method-level annotation on @RequestMapping
methods, in which case the return value of the @RequestMapping method is interpreted as a
model attribute. This is typically not required, as it is the default behavior in HTML
controllers, unless the return value is a String that would otherwise be interpreted
as a view name. @ModelAttribute can also help to customize the model attribute name,
as the following example shows:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.4.5. DataBinder
@Controller or @ControllerAdvice classes can have @InitBinder methods, to
initialize instances of WebDataBinder. Those, in turn, are used to:
-
Bind request parameters (that is, form data or query) to a model object.
-
Convert
String-based request values (such as request parameters, path variables, headers, cookies, and others) to the target type of controller method arguments. -
Format model object values as
Stringvalues when rendering HTML forms.
@InitBinder methods can register controller-specific java.beans.PropertyEditor or
Spring Converter and Formatter components. In addition, you can use the
WebFlux Java configuration to register Converter and
Formatter types in a globally shared FormattingConversionService.
@InitBinder methods support many of the same arguments that @RequestMapping methods
do, except for @ModelAttribute (command object) arguments. Typically, they are declared
with a WebDataBinder argument, for registrations, and a void return value.
The following example uses the @InitBinder annotation:
@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 | Using the @InitBinder annotation. |
@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))
}
// ...
}
Alternatively, when using a Formatter-based setup through a shared
FormattingConversionService, you could re-use the same approach and register
controller-specific Formatter instances, as the following example shows:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
| 1 | Adding a custom formatter (a DateFormatter, in this case). |
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
| 1 | Adding a custom formatter (a DateFormatter, in this case). |
Model Design
In the context of web applications, data binding involves the binding of HTTP request parameters (that is, form data or query parameters) to properties in a model object and its nested objects.
Only public properties following the
JavaBeans naming conventions [Oracle] (英語)
are exposed for data binding — for example, public String getFirstName() and
public void setFirstName(String) methods for a firstName property.
| The model object, and its nested object graph, is also sometimes referred to as a command object, form-backing object, or POJO (Plain Old Java Object). |
By default, Spring permits binding to all public properties in the model object graph. This means you need to carefully consider what public properties the model has, since a client could target any public property path, even some that are not expected to be targeted for a given use case.
For example, given an HTTP form data endpoint, a malicious client could supply values for properties that exist in the model object graph but are not part of the HTML form presented in the browser. This could lead to data being set on the model object and any of its nested objects, that is not expected to be updated.
The recommended approach is to use a dedicated model object that exposes only
properties that are relevant for the form submission. For example, on a form for changing
a user’s email address, the model object should declare a minimum set of properties such
as in the following ChangeEmailForm.
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
If you cannot or do not want to use a dedicated model object for each data
binding use case, you must limit the properties that are allowed for data binding.
Ideally, you can achieve this by registering allowed field patterns via the
setAllowedFields() method on WebDataBinder.
For example, to register allowed field patterns in your application, you can implement an
@InitBinder method in a @Controller or @ControllerAdvice component as shown below:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
In addition to registering allowed patterns, it is also possible to register disallowed
field patterns via the setDisallowedFields() method in DataBinder and its subclasses.
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
setAllowedFields() should be favored over setDisallowedFields().
Note that matching against allowed field patterns is case-sensitive; whereas, matching against disallowed field patterns is case-insensitive. In addition, a field matching a disallowed pattern will not be accepted even if it also happens to match a pattern in the allowed list.
|
It is extremely important to properly configure allowed and disallowed field patterns when exposing your domain model directly for data binding purposes. Otherwise, it is a big security risk. Furthermore, it is strongly recommended that you do not use types from your domain model such as JPA or Hibernate entities as the model object in data binding scenarios. |
1.4.6. Managing Exceptions
@Controller and @ControllerAdvice classes can have
@ExceptionHandler methods to handle exceptions from controller methods. The following
example includes such a handler method:
@Controller
public class SimpleController {
// ...
@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
| 1 | Declaring an @ExceptionHandler. |
@Controller
class SimpleController {
// ...
@ExceptionHandler (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
| 1 | Declaring an @ExceptionHandler. |
The exception can match against a top-level exception being propagated (that is, a direct
IOException being thrown) or against the immediate cause within a top-level wrapper
exception (for example, an IOException wrapped inside an IllegalStateException).
For matching exception types, preferably declare the target exception as a method argument,
as shown in the preceding example. Alternatively, the annotation declaration can narrow the
exception types to match. We generally recommend being as specific as possible in the
argument signature and to declare your primary root exception mappings on a
@ControllerAdvice prioritized with a corresponding order.
See the MVC section for details.
An @ExceptionHandler method in WebFlux supports the same method arguments and
return values as a @RequestMapping method, with the exception of request body-
and @ModelAttribute-related method arguments.
|
Support for @ExceptionHandler methods in Spring WebFlux is provided by the
HandlerAdapter for @RequestMapping methods. See DispatcherHandler
for more detail.
REST API exceptions
A common requirement for REST services is to include error details in the body of the
response. The Spring Framework does not automatically do so, because the representation
of error details in the response body is application-specific. However, a
@RestController can use @ExceptionHandler methods with a ResponseEntity return
value to set the status and the body of the response. Such methods can also be declared
in @ControllerAdvice classes to apply them globally.
Note that Spring WebFlux does not have an equivalent for the Spring MVC
ResponseEntityExceptionHandler, because WebFlux raises only ResponseStatusException
(or subclasses thereof), and those do not need to be translated to
an HTTP status code.
|
1.4.7. Controller Advice
Typically, the @ExceptionHandler, @InitBinder, and @ModelAttribute methods apply
within the @Controller class (or class hierarchy) in which they are declared. If you
want such methods to apply more globally (across controllers), you can declare them in a
class annotated with @ControllerAdvice or @RestControllerAdvice.
@ControllerAdvice is annotated with @Component, which means that such classes can be
registered as Spring beans through component scanning. @RestControllerAdvice is a composed annotation that is annotated
with both @ControllerAdvice and @ResponseBody, which essentially means
@ExceptionHandler methods are rendered to the response body through message conversion
(versus view resolution or template rendering).
On startup, the infrastructure classes for @RequestMapping and @ExceptionHandler
methods detect Spring beans annotated with @ControllerAdvice and then apply their
methods at runtime. Global @ExceptionHandler methods (from a @ControllerAdvice) are
applied after local ones (from the @Controller). By contrast, global @ModelAttribute
and @InitBinder methods are applied before local ones.
By default, @ControllerAdvice methods apply to every request (that is, all controllers),
but you can narrow that down to a subset of controllers by using attributes on the
annotation, as the following example shows:
// 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 {}
// 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 {}
The selectors in the preceding example are evaluated at runtime and may negatively impact
performance if used extensively. See the
@ControllerAdvice (Javadoc)
javadoc for more details.
1.5. Functional Endpoints
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions are used to route and handle requests and contracts are designed for immutability. It is an alternative to the annotation-based programming model but otherwise runs on the same Reactive Core foundation.
1.5.1. Overview
In WebFlux.fn, an HTTP request is handled with a HandlerFunction: a function that takes
ServerRequest and returns a delayed ServerResponse (i.e. Mono<ServerResponse>).
Both the request and the response object have immutable contracts that offer JDK 8-friendly
access to the HTTP request and response.
HandlerFunction is the equivalent of the body of a @RequestMapping method in the
annotation-based programming model.
Incoming requests are routed to a handler function with a RouterFunction: a function that
takes ServerRequest and returns a delayed HandlerFunction (i.e. Mono<HandlerFunction>).
When the router function matches, a handler function is returned; otherwise an empty Mono.
RouterFunction is the equivalent of a @RequestMapping annotation, but with the major
difference that router functions provide not just data, but also behavior.
RouterFunctions.route() provides a router builder that facilitates the creation of routers,
as the following example shows:
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) {
// ...
}
}
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 {
// ...
}
}
| 1 | Create router using Coroutines router DSL, a Reactive alternative is also available via router { }. |
One way to run a RouterFunction is to turn it into an HttpHandler and install it
through one of the built-in server adapters:
-
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> に抽出します。
Mono<String> string = request.bodyToMono(String.class);val string = request.awaitBody<String>() 次の例では、本文を Flux<Person> (または Kotlin の Flow<Person>)に抽出します。ここで、Person オブジェクトは、JSON や XML などの直列化された形式からデコードされます。
Flux<Person> people = request.bodyToFlux(Person.class);val people = request.bodyToFlow<Person>() 上記の例は、より一般的な ServerRequest.body(BodyExtractor) を使用するショートカットであり、BodyExtractor 機能戦略インターフェースを受け入れます。ユーティリティクラス BodyExtractors は、多数のインスタンスへのアクセスを提供します。例: 上記の例は、次のように書くこともできます。
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class)); val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()次の例は、フォームデータにアクセスする方法を示しています。
Mono<MultiValueMap<String, String>> map = request.formData();val map = request.awaitFormData()次の例は、マップとしてマルチパートデータにアクセスする方法を示しています。
Mono<MultiValueMap<String, Part>> map = request.multipartData();val map = request.awaitMultipartData()次の例は、ストリーミング方式で一度に 1 つずつマルチパートにアクセスする方法を示しています。
Flux<Part> parts = request.body(BodyExtractors.toParts());val parts = request.body(BodyExtractors.toParts()).asFlow()ServerResponse
ServerResponse は HTTP レスポンスへのアクセスを提供します。これは不変なので、build メソッドを使用して HTTP レスポンスを作成できます。ビルダーを使用して、レスポンスステータスを設定したり、レスポンスヘッダーを追加したり、本文を提供したりできます。次の例では、JSON コンテンツで 200(OK)レスポンスを作成します。
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person) 次の例は、Location ヘッダーを持ち、本文がない 201(CREATED)レスポンスを作成する方法を示しています。
URI location = ...
ServerResponse.created(location).build();val location: URI = ...
ServerResponse.created(location).build()使用されるコーデックに応じて、ヒントパラメーターを渡して、本体の直列化または逆直列化の方法をカスタマイズできます。例: Jackson JSON ビュー (英語) を指定するには:
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)ハンドラークラス
次の例に示すように、ハンドラー関数をラムダとして記述できます。
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") } これは便利ですが、アプリケーションでは複数の関数が必要であり、複数のインラインラムダが乱雑になる可能性があります。関連するハンドラー関数を、アノテーションベースのアプリケーションの @Controller と同様のロールを持つハンドラークラスにグループ化すると便利です。例: 次のクラスは、リアクティブ Person リポジトリを公開します。
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());
}
}| 1 | listPeople は、リポジトリで見つかったすべての Person オブジェクトを JSON として返すハンドラー関数です。 |
| 2 | createPerson は、リクエスト本文に含まれる新しい Person を保存するハンドラー関数です。PersonRepository.savePerson(Person) は Mono<Void> を返すことに注意してください。空の Mono は、リクエストから人が読み取られて保管されたときに完了シグナルを発行します。そのため、build(Publisher<Void>) メソッドを使用して、完了シグナルを受信したとき(つまり、Person が保存されたとき)にレスポンスを送信します。 |
| 3 | getPerson は、id パス変数で識別される 1 人の人物を返すハンドラー関数です。Person をリポジトリから取得し、見つかった場合は JSON レスポンスを作成します。見つからない場合は、switchIfEmpty(Mono<T>) を使用して 404 未検出レスポンスを返します。 |
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()
}
}| 1 | listPeople は、リポジトリで見つかったすべての Person オブジェクトを JSON として返すハンドラー関数です。 |
| 2 | createPerson は、リクエスト本文に含まれる新しい Person を保存するハンドラー関数です。PersonRepository.savePerson(Person) は、戻り値のない一時停止関数であることに注意してください。 |
| 3 | getPerson は、id パス変数で識別される 1 人の人物を返すハンドラー関数です。Person をリポジトリから取得し、見つかった場合は JSON レスポンスを作成します。見つからない場合、404 未検出レスポンスを返します。 |
検証
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)
}
}
}| 1 | Validator インスタンスを作成します。 |
| 2 | 検証を適用します。 |
| 3 | 400 レスポンスに対して例外を発生させます。 |
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)
}
}
}| 1 | Validator インスタンスを作成します。 |
| 2 | 検証を適用します。 |
| 3 | 400 レスポンスに対して例外を発生させます。 |
ハンドラーは、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 ヘッダーに基づいて制約を作成します。
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();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 番目のルートが評価されます。一般的なルートの前に、より具体的なルートを宣言することは理にかなっています。これは、後で説明するように、ルーター関数を Spring Bean として登録する場合にも重要です。この動作は、「最も具体的な」コントローラーメソッドが自動的に選択されるアノテーションベースのプログラミングモデルとは異なることに注意してください。
ルーター関数ビルダーを使用する場合、定義されたすべてのルートは、build() から返される 1 つの RouterFunction に構成されます。複数のルーター関数を一緒に構成する他の方法もあります。
RouterFunctions.route()ビルダーのadd(RouterFunction)RouterFunction.and(RouterFunction)RouterFunction.andRoute(RequestPredicate, HandlerFunction)—RouterFunctions.route()をネストしたRouterFunction.and()のショートカット。
次の例は、4 つのルートの構成を示しています。
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 にマップされます |
| 4 | otherRoute は、他の場所で作成され、構築されたルートに追加されるルーター関数です。 |
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 にマップされます |
| 4 | otherRoute は、他の場所で作成され、構築されたルートに追加されるルーター関数です。 |
ネストされたルート
ルーター関数のグループが共有述語(共有パスなど)を持つことは一般的です。上記の例では、共有述語は、3 つのルートで使用される /person に一致するパス述語になります。アノテーションを使用する場合、/person にマップする型レベルの @RequestMapping アノテーションを使用して、この重複を削除します。WebFlux.fn では、経路述語は、ルーター関数ビルダーの path メソッドを介して共有できます。たとえば、上記の例の最後の数行は、ネストされたルートを使用して次のように改善できます。
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();| 1 | path の 2 番目のパラメーターは、ルータービルダーを使用するコンシューマーであることに注意してください。 |
val route = coRouter {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
} パスベースのネストが最も一般的ですが、Builder で nest メソッドを使用して、あらゆる種類の述語にネストできます。上記には、共有 Accept -header 述語の形式での重複がまだ含まれています。nest メソッドと accept を併用すると、さらに改善できます。
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
POST(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にリクエストをルーティングします。HandlerFunctionAdapter:DispatcherHandlerがリクエストにマップされたHandlerFunctionを呼び出せるようにする単純なアダプター。ServerResponseResultHandler:ServerResponseのwriteToメソッドを呼び出すことにより、HandlerFunctionの呼び出しからの結果を処理します。
上記のコンポーネントにより、関数エンドポイントは DispatcherHandler リクエスト処理ライフサイクルに適合し、アノテーション付きコントローラー(存在する場合)と並行して(潜在的に)実行されます。また、Spring Boot WebFlux スターターが関数エンドポイントを有効にする方法でもあります。
次の例は、WebFlux Java 構成を示しています(実行方法については、DispatcherHandler を参照してください)。
@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...
}
}@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. ハンドラー関数のフィルタリング
ルーティング関数ビルダーで before、after、filter メソッドを使用して、ハンドラー関数をフィルター処理できます。アノテーションを使用すると、@ControllerAdvice、ServletFilter、その両方を使用して同様の機能を実現できます。フィルターは、ビルダーによって作成されたすべてのルートに適用されます。これは、ネストされたルートで定義されたフィルターが「トップレベル」ルートに適用されないことを意味します。たとえば、次の例について考えてみます。
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(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();| 1 | カスタムリクエストヘッダーを追加する before フィルターは、2 つの GET ルートにのみ適用されます。 |
| 2 | レスポンスを記録する after フィルターは、ネストされたものを含むすべてのルートに適用されます。 |
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
POST(handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
}| 1 | カスタムリクエストヘッダーを追加する before フィルターは、2 つの GET ルートにのみ適用されます。 |
| 2 | レスポンスを記録する after フィルターは、ネストされたものを含むすべてのルートに適用されます。 |
ルータービルダーの filter メソッドは HandlerFilterFunction を取ります。これは、ServerRequest と HandlerFunction を取り、ServerResponse を返す関数です。ハンドラー関数パラメーターは、チェーンの次の要素を表します。これは通常、ルーティング先のハンドラーですが、複数が適用される場合は別のフィルターにすることもできます。
特定のパスが許可されているかどうかを判断できる SecurityManager があると仮定して、ルートに簡単なセキュリティフィルターを追加できます。次の例は、その方法を示しています。
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(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 を作成できます。
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)| 1 | URI テンプレートを使用した静的ファクトリメソッド。 |
| 2 | URI コンポーネントを追加または置換します。 |
| 3 | URI テンプレートと URI 変数をエンコードするようリクエストします。 |
| 4 | UriComponents をビルドします。 |
| 5 | 変数を展開し、URI を取得します。 |
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)| 1 | URI テンプレートを使用した静的ファクトリメソッド。 |
| 2 | URI コンポーネントを追加または置換します。 |
| 3 | URI テンプレートと URI 変数をエンコードするようリクエストします。 |
| 4 | UriComponents をビルドします。 |
| 5 | 変数を展開し、URI を取得します。 |
前述の例は、次の例に示すように、1 つのチェーンに統合し、buildAndExpand で短縮できます。
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()次の例に示すように、URI に直接移動することで(エンコードを暗示する)、さらに短くすることができます。
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")次の例に示すように、完全な URI テンプレートを使用してさらに短縮できます。
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");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 を構成する方法を示しています。
// 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);// 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 を構成します。
// 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();// 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 の使用に似ていますが、次の例に示すように、静的ファクトリメソッドの代わりに、構成と設定を保持する実際のインスタンスです。
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");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 つのレベルでエンコードオプションを公開します。
UriComponentsBuilder#encode() (Javadoc) : 最初に URI テンプレートを事前にエンコードし、次に展開時に URI 変数を厳密にエンコードします。
UriComponents#encode() (Javadoc) : URI 変数が展開された後、 URI コンポーネントをエンコードします。
どちらのオプションも、非 ASCII 文字と不正な文字をエスケープされたオクテットに置き換えます。ただし、最初のオプションは、URI 変数に表示される予約された意味で文字を置き換えます。
| ";" を検討してください。これはパスでは有効ですが、意味は予約されています。最初のオプションは ";" を置き換えます。URI 変数には "%3B" が含まれますが、URI テンプレートには含まれません。対照的に、2 番目のオプションはパス内の正当な文字であるため、";" を置き換えることはありません。 |
ほとんどの場合、最初のオプションは URI 変数を完全にエンコードされる不透明(OPAQUE)データとして扱うため、期待どおりの結果が得られる可能性があります。2 番目のオプションは、URI 変数に意図的に予約文字が含まれている場合に役立ちます。2 番目のオプションは、URI 変数をまったく展開しない場合にも役立ちます。これは、偶然に URI 変数のように見えるものもエンコードするためです。
次の例では、最初のオプションを使用しています。
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"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 に直接移動することで、前述の例を短縮できます(エンコードを意味します)。
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")次の例に示すように、完全な URI テンプレートを使用してさらに短縮できます。
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")WebClient と RestTemplate は、UriBuilderFactory 戦略を通じて内部で URI テンプレートを拡張およびエンコードします。次の例に示すように、どちらもカスタム戦略で構成できます。
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();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#encodeUriVariablesを使用して 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 がどのように機能するかを学ぶために、他の多くの人と一緒にこの記事 [Mozilla] を読むか、詳細について仕様を参照してください。
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) を参照してください。
ソースから詳細を確認するか、高度なカスタマイズを行うには、次を参照してください。
|
1.7.3. 資格証明付きリクエスト
認証情報付きリクエストで CORS を使用するには、allowedCredentials を有効にする必要があります。このオプションでは、構成されたドメインとの高レベルの信頼が確立され、Cookie や CSRF トークンなどのユーザー固有の機密情報が公開されるため、Web アプリケーションの攻撃対象領域が増えることに注意してください。
資格情報を有効にすると、構成された "*" CORS ワイルドカードの処理方法にも影響します。
allowOriginsではワイルドカードは認可されていませんが、代わりにallowOriginPatternsプロパティを使用して、起点の動的なセットと一致させることができます。allowedHeadersまたはallowedMethodsに設定すると、Access-Control-Allow-HeadersおよびAccess-Control-Allow-Methodsレスポンスヘッダーは、CORS プリフライトリクエストで指定された関連ヘッダーとメソッドをコピーすることによって処理されます。exposedHeadersに設定すると、Access-Control-Expose-Headersレスポンスヘッダーは、構成されたヘッダーのリストまたはワイルドカード文字のいずれかに設定されます。CORS 仕様では、Access-Control-Allow-Credentialsがtrueに設定されている場合にワイルドカード文字を使用できませんが、ほとんどのブラウザーはワイルドカード文字をサポートしており、CORS 処理中にレスポンスヘッダーがすべて使用できるわけではないため、結果として、ワイルドカード文字は、指定時に使用されるヘッダー値になります。allowCredentialsプロパティの値。
| このようなワイルドカード構成は便利ですが、可能であれば、より高いレベルのセキュリティを提供するために、代わりに有限の値のセットを構成することをお勧めします。 |
1.7.4. @CrossOrigin
@CrossOrigin (Javadoc) アノテーションは、次の例に示すように、アノテーション付きコントローラーメソッドでクロスオリジンリクエストを有効にします。
@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) {
// ...
}
}@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 メソッド。
allowCredentials はデフォルトでは有効になっていません。これは、ユーザー固有の機密情報(Cookie や CSRF トークンなど)を公開する信頼レベルを確立し、適切な場合にのみ使用する必要があるためです。有効になっている場合は、allowOrigins を 1 つ以上の特定のドメインに設定する必要があります(ただし、特別な値 "*" は設定しないでください)。または、allowOriginPatterns プロパティを使用して動的な起点のセットに一致させることもできます。
maxAge は 30 分に設定されています。
@CrossOrigin はクラスレベルでもサポートされ、すべてのメソッドに継承されます。次の例では、特定のドメインを指定し、maxAge を 1 時間に設定します。
@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) {
// ...
}
}@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 はクラスレベルとメソッドレベルの両方で使用できます。
@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 を使用します。 |
@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.5. グローバル構成
細分化されたコントローラーメソッドレベルの構成に加えて、おそらくグローバルな CORS 構成も定義する必要があります。URL ベースの CorsConfiguration マッピングは、HandlerMapping で個別に設定できます。ただし、ほとんどのアプリケーションは、WebFlux Java 構成を使用してこれを実行します。
デフォルトでは、グローバル構成により以下が有効になります。
すべての起源。
すべてのヘッダー。
GET、HEAD、POSTメソッド。
allowedCredentials はデフォルトでは有効になっていません。これは、ユーザー固有の機密情報(Cookie や CSRF トークンなど)を公開する信頼レベルを確立し、適切な場合にのみ使用する必要があるためです。有効になっている場合は、allowOrigins を 1 つ以上の特定のドメインに設定する必要があります(ただし、特別な値 "*" は設定しないでください)。または、allowOriginPatterns プロパティを使用して動的な起点のセットに一致させることもできます。
maxAge は 30 分に設定されています。
WebFlux Java 構成で CORS を有効にするには、次の例に示すように、CorsRegistry コールバックを使用できます。
@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...
}
}@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.6. CORS WebFilter
組み込みの CorsWebFilter (Javadoc) を使用して CORS サポートを適用できます。これは、関数エンドポイントに適しています。
CorsFilter を Spring Security で使用しようとする場合、Spring Security には CORS のサポートが組み込まれていることに注意してください。 |
フィルターを構成するには、次の例に示すように、CorsWebFilter Bean を宣言し、CorsConfigurationSource をコンストラクターに渡すことができます。
@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);
}@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 プロジェクトによって管理されます。構成には、SpringResourceTemplateResolver、SpringWebFluxTemplateEngine、ThymeleafReactiveViewResolver などのいくつかの Bean 宣言が含まれます。詳細については、Thymeleaf+Spring (英語) および WebFlux 統合の発表 (英語) を参照してください。
1.9.2. FreeMarker
Apache FreeMarker (英語) は、HTML からメールなどへのあらゆる種類のテキスト出力を生成するためのテンプレートエンジンです。Spring Framework には、Spring WebFlux と FreeMarker テンプレートを使用するための組み込みの統合機能があります。
構成を表示
次の例は、FreeMarker をビューテクノロジーとして設定する方法を示しています。
@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;
}
}@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 の使用方法を示しています。
@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;
}
}@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 の簡単なバインドを参照してください。
1.9.3. スクリプトビュー
Spring Framework には、JSR-223 (英語) Java スクリプトエンジン上で実行できるテンプレートライブラリで Spring WebFlux を使用するための組み込みの統合があります。次の表は、さまざまなスクリプトエンジンでテストしたテンプレートライブラリを示しています。
| スクリプトライブラリ | スクリプトエンジン |
|---|---|
他のスクリプトエンジンを統合するための基本的なルールは、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 エンジンを使用しています。
@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;
}
}@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 (英語) は、使用する前にテンプレートをコンパイルする必要があり、サーバーサイドスクリプトエンジンでは利用できないブラウザー機能をエミュレートするために、ポリフィル [Mozilla] が必要です。次の例は、カスタムレンダリング関数を設定する方法を示しています。
@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;
}
}@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 (参考: JavaScript フロントエンドフレームワークの比較 [Qiita] ) など、同時実行用に設計されていないテンプレートライブラリで非スレッドセーフスクリプトエンジンを使用する場合は、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 を提供します。これを使用して、Jackson2JsonEncoder、Jackson2SmileEncoder、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 [IETF] (英語) は Cache-Control レスポンスヘッダーのすべての可能なディレクティブを記述しますが、CacheControl 型は、次の例に示すように、一般的なシナリオに焦点を当てたユースケース指向のアプローチを取ります。
// 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();// 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 に追加できます。
@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);
}@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 ヘッダーがレスポンスに追加されます。
次の例に示すように、コントローラーの条件付きリクエストヘッダーに対してチェックを行うこともできます。
@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 | リクエスト処理を続行します。 |
@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)に設定できます。条件付き POST、PUT、DELETE の場合、代わりにレスポンスを 412(PRECONDITION_FAILED)に設定して、同時変更を防ぐことができます。
1.11. WebFlux 構成
WebFlux Java 構成は、アノテーション付きコントローラーまたは関数エンドポイントでリクエストを処理するために必要なコンポーネントを宣言し、構成をカスタマイズするための API を提供します。つまり、Java 構成によって作成された基礎となる Bean を理解する必要はありません。ただし、理解したい場合は、WebFluxConfigurationSupport で表示するか、特別な Bean 型の内容について詳しく読むことができます。
構成 API にはない高度なカスタマイズについては、拡張構成モードを使用して構成を完全に制御できます。
1.11.1. WebFlux 構成の有効化
次の例に示すように、Java config で @EnableWebFlux アノテーションを使用できます。
@Configuration
@EnableWebFlux
public class WebConfig {
}@Configuration
@EnableWebFlux
class WebConfig上記の例では、多数の Spring WebFlux インフラストラクチャ Bean を登録し、JSON、XML などのクラスパスで利用可能な依存関係に適応しています。
1.11.2. WebFlux 構成 API
Java 構成では、次の例に示すように、WebFluxConfigurer インターフェースを実装できます。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// Implement configuration methods...
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// Implement configuration methods...
}1.11.3. 変換、フォーマット
デフォルトでは、さまざまな数値および日付型のフォーマッタがインストールされ、フィールドでの @NumberFormat および @DateTimeFormat によるカスタマイズのサポートがインストールされています。
Java config でカスタムフォーマッタとコンバーターを登録するには、次を使用します。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}デフォルトでは、Spring WebFlux は、日付値を解析およびフォーマットするときに、リクエストロケールを考慮します。これは、日付が「入力」フォームフィールドを持つ文字列として表されるフォームで機能します。ただし、「日付」および「時刻」フォームフィールドの場合、ブラウザーは HTML 仕様で定義されている固定形式を使用します。このような場合、日付と時刻のフォーマットは次のようにカスタマイズできます。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}@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 インスタンスをカスタマイズできます。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public Validator getValidator() {
// ...
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun getValidator(): Validator {
// ...
}
} 次の例に示すように、Validator 実装をローカルに登録することもできます。
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}LocalValidatorFactoryBean をどこかに挿入する必要がある場合は、Bean を作成し、@Primary でマークして、MVC config で宣言されたものとの競合を回避します。 |
1.11.5. コンテンツ型リゾルバー
Spring WebFlux が、リクエストから @Controller インスタンスのリクエストされたメディア型を決定する方法を設定できます。デフォルトでは、Accept ヘッダーのみがチェックされますが、クエリパラメーターベースの戦略を有効にすることもできます。
次の例は、リクエストされたコンテンツ型の解決をカスタマイズする方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
// ...
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
// ...
}
}1.11.6. HTTP メッセージコーデック
次の例は、リクエストとレスポンスの本文の読み取りおよび書き込み方法をカスタマイズする方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
configurer.defaultCodecs().maxInMemorySize(512 * 1024);
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
// ...
}
}ServerCodecConfigurer は、デフォルトのリーダーとライターのセットを提供します。これを使用して、リーダーとライターを追加したり、デフォルトのものをカスタマイズしたり、デフォルトのものを完全に置き換えたりできます。
Jackson JSON および XML の場合、Jackson2ObjectMapperBuilder (Javadoc) の使用を検討してください。Jackson2ObjectMapperBuilder (Javadoc) は、Jackson のデフォルトプロパティを次のようにカスタマイズします。
また、次の既知のモジュールがクラスパスで検出された場合、自動的に登録します。
jackson-datatype-joda[GitHub] (英語) : Joda-Time 型のサポート。jackson-datatype-jsr310[GitHub] (英語) : Java 8 Date and Time API 型のサポート。jackson-datatype-jdk8[GitHub] (英語) :Optionalなど、他の Java 8 型のサポート。jackson-module-kotlin[GitHub] (英語) : Kotlin クラスとデータクラスのサポート。
1.11.7. リゾルバーを表示
次の例は、ビューリゾルバーを構成する方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// ...
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// ...
}
}ViewResolverRegistry には、Spring Framework が統合するビューテクノロジーへのショートカットがあります。次の例では、FreeMarker を使用しています(これには、基になる FreeMarker ビューテクノロジの構成も必要です)
@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;
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure Freemarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("classpath:/templates")
}
} 次の例に示すように、ViewResolver 実装をプラグインすることもできます。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ViewResolver resolver = ... ;
registry.viewResolver(resolver);
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
val resolver: ViewResolver = ...
registry.viewResolver(resolver
}
} コンテンツネゴシエーションをサポートし、ビューリゾルバー (HTML 以外) を介して他の形式をレンダリングするには、spring-web から利用可能なコーデックのいずれかを受け入れる HttpMessageWriterView 実装に基づいて、1 つまたは複数の既定のビューを構成できます。次の例は、その方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
// ...
}@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 ステータスコードが返されます。次のリストは、例を示しています。
@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));
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
}
}静的リソースの HTTP キャッシュサポートも参照してください。
リソースハンドラーは、ResourceResolver (Javadoc) 実装と ResourceTransformer (Javadoc) 実装のチェーンもサポートしています。これらを使用して、最適化されたリソースを操作するためのツールチェーンを作成できます。
コンテンツ、固定アプリケーションバージョン、その他の情報から計算された MD5 ハッシュに基づいて、バージョン管理されたリソース URL に VersionResourceResolver を使用できます。ContentVersionStrategy (MD5 ハッシュ)は、いくつかの注目すべき例外(モジュールローダーで使用される JavaScript リソースなど)がある場合に適しています。
次の例は、Java 構成で VersionResourceResolver を使用する方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}@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 (英語) の場合、/webjars/jquery/1.2.0/jquery.min.js のようなバージョン管理された URL が推奨され、最も効率的な使用方法です。関連するリソースの場所は Spring Boot を使用してすぐに構成でき (または ResourceHandlerRegistry を介して手動で構成できます)、org.webjars:webjars-locator-core 依存関係を追加する必要はありません。
/webjars/jquery/jquery.min.js のようなバージョンのない URL は、org.webjars:webjars-locator-core ライブラリがクラスパスに存在する場合に自動的に登録される WebJarsResourceResolver を介してサポートされますが、アプリケーションの起動を遅くする可能性のあるクラスパススキャンが犠牲になります。リゾルバーは、jar のバージョンを含むように URL を書き換えることができます。また、バージョンのない受信 URL (たとえば、/webjars/jquery/jquery.min.js から /webjars/jquery/1.2.0/jquery.min.js へ) と照合することもできます。
ResourceHandlerRegistry に基づく Java 構成は、きめ細かい制御のための追加オプションを提供します。最終変更された動作と最適化されたリソース解決。 |
1.11.9. パスマッチング
パスマッチングに関連するオプションをカスタマイズできます。個々のオプションの詳細については、PathMatchConfigurer javadoc を参照してください。次の例は、PathMatchConfigurer の使用方法を示しています。
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController::class.java))
}
}Spring WebFlux は、デコードされたパスセグメント値にアクセスするために、 Spring WebFlux はまた、またしている Spring MVC、とは異なり、接尾辞パターンマッチングをサポートしていないお勧めします、それに離れ依存から移動します。 |
1.11.10. WebSocketService
WebFlux Java 構成は、WebSocket ハンドラーの呼び出しのサポートを提供する WebSocketHandlerAdapter Bean を宣言します。つまり、WebSocket ハンドシェイクリクエストを処理するために必要なことは、WebSocketHandler を SimpleUrlHandlerMapping を介して URL にマップすることだけです。
場合によっては、WebSocket サーバーのプロパティを構成できる WebSocketService サービスを提供して WebSocketHandlerAdapter Bean を作成する必要があります。例:
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public WebSocketService getWebSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Override
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}1.11.11. 拡張構成モード
@EnableWebFlux は DelegatingWebFluxConfiguration をインポートします:
WebFlux アプリケーションにデフォルトの Spring 構成を提供します
WebFluxConfigurer実装を検出して委譲し、その構成をカスタマイズします。
拡張モードの場合、次の例に示すように、WebFluxConfigurer を実装する代わりに、@EnableWebFlux を削除して DelegatingWebFluxConfiguration から直接拡張できます。
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {
// ...
}@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 には、Reactor に基づく関数で流れるような API があります。リアクティブライブラリを参照してください。これにより、スレッドや同時実行性を処理することなく、非同期ロジックの宣言型合成が可能になります。これは完全にノンブロッキングであり、ストリーミングをサポートし、サーバー側でリクエストとレスポンスのコンテンツをエンコードおよびデコードするためにも使用されるのと同じコーデックに依存しています。
WebClient には、リクエストを実行するための HTTP クライアントライブラリが必要です。以下のサポートが組み込まれています。
その他は
ClientHttpConnectorを介して接続できます。
2.1. 構成
WebClient を作成する最も簡単な方法は、静的ファクトリメソッドの 1 つを使用することです。
WebClient.create()WebClient.create(String baseUrl)
WebClient.builder() を追加オプションとともに使用することもできます。
uriBuilderFactory: ベース URL として使用するためにUriBuilderFactoryをカスタマイズしました。defaultUriVariables: URI テンプレートを展開するときに使用するデフォルト値。defaultHeader: すべてのリクエストのヘッダー。defaultCookie: リクエストごとの Cookie。defaultRequest:Consumerですべてのリクエストをカスタマイズします。filter: すべてのリクエストのクライアントフィルター。exchangeStrategies: HTTP メッセージリーダー / ライターのカスタマイズ。clientConnector: HTTP クライアントライブラリの設定。
例:
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();val webClient = WebClient.builder()
.codecs { configurer -> ... }
.build() 一度構築されると、WebClient は不変です。ただし、次のようにクローンを作成して、変更されたコピーを作成できます。
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, filterDval 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, filterD2.1.1. MaxInMemorySize
コーデックには、アプリケーションのメモリの課題を回避するために、メモリにデータをバッファリングするための制限があります。デフォルトでは、これらは 256KB に設定されています。それでも不十分な場合は、次のエラーが発生します。
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
デフォルトコーデックの制限を変更するには、以下を使用します。
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();val webClient = WebClient.builder()
.codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
.build()2.1.2. Reactor Netty
Reactor Netty 設定をカスタマイズするには、事前構成された HttpClient を提供します。
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();val httpClient = HttpClient.create().secure { ... }
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()リソース
デフォルトでは、HttpClient は、イベントループスレッドや接続プールなど、reactor.netty.http.HttpResources に保持されているグローバル Reactor Netty リソースに参加します。これは、イベントループの同時実行には固定の共有リソースが優先されるため、推奨されるモードです。このモードでは、プロセスが終了するまでグローバルリソースがアクティブのままになります。
サーバーがプロセスとタイミングを合わせている場合、通常、明示的なシャットダウンは必要ありません。ただし、サーバーがインプロセス (たとえば、WAR としてデプロイされた Spring MVC アプリケーション) を開始または停止できる場合は、ReactorResourceFactory 型の Spring 管理 Bean を globalResources=true (デフォルト) で宣言して、Reactor Netty グローバルリソース次の例に示すように、Spring ApplicationContext が閉じられるとシャットダウンされます。
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}@Bean
fun reactorResourceFactory() = ReactorResourceFactory()グローバル Reactor Netty リソースに参加しないことも選択できます。ただし、このモードでは、次の例に示すように、すべての Reactor Netty クライアントおよびサーバーインスタンスが共有リソースを使用することを保証する負担があります。
@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 に差し込みます。 |
@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 に差し込みます。 |
タイムアウト
接続タイムアウトを構成するには:
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();import io.netty.channel.ChannelOption
val httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
val webClient = WebClient.builder()
.clientConnector(ReactorClientHttpConnector(httpClient))
.build();読み取りまたは書き込みタイムアウトを構成するには:
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler
val httpClient = HttpClient.create()
.doOnConnected { conn -> conn
.addHandlerLast(ReadTimeoutHandler(10))
.addHandlerLast(WriteTimeoutHandler(10))
}
// Create WebClient...すべてのリクエストのレスポンスタイムアウトを設定するには:
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...val httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...特定のリクエストのレスポンスタイムアウトを設定するには:
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);WebClient.create().get()
.uri("https://example.org/path")
.httpRequest { httpRequest: ClientHttpRequest ->
val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
reactorRequest.responseTimeout(Duration.ofSeconds(2))
}
.retrieve()
.bodyToMono(String::class.java)2.1.3. Jetty
次の例は、Jetty HttpClient 設定をカスタマイズする方法を示しています。
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();val httpClient = HttpClient()
httpClient.cookieStore = ...
val webClient = WebClient.builder()
.clientConnector(JettyClientHttpConnector(httpClient))
.build(); デフォルトでは、HttpClient は独自のリソース(Executor、ByteBufferPool、Scheduler)を作成し、プロセスが終了するか stop() が呼び出されるまでアクティブのままになります。
次の例に示すように、型 JettyResourceFactory の Spring 管理 Bean を宣言することにより、Jetty クライアント(およびサーバー)の複数のインスタンス間でリソースを共有し、Spring ApplicationContext が閉じられたときにリソースを確実にシャットダウンできます。
@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 に差し込みます。 |
@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.1.4. HttpComponents
次の例は、Apache HttpComponents HttpClient 設定をカスタマイズする方法を示しています。
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();val client = HttpAsyncClients.custom().apply {
setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()2.2. retrieve()
retrieve() メソッドを使用して、レスポンスを抽出する方法を宣言できます。例:
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity<Person>().awaitSingle()または体だけを取得するには:
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);val client = WebClient.create("https://example.org")
val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.awaitBody<Person>()デコードされたオブジェクトのストリームを取得するには:
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);val result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlow<Quote>() デフォルトでは、4xx または 5xx レスポンスは、特定の HTTP ステータスコードのサブクラスを含む WebClientResponseException になります。エラーレスポンスの処理をカスタマイズするには、次のように onStatus ハンドラーを使用します。
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);val result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError) { ... }
.onStatus(HttpStatus::is5xxServerError) { ... }
.awaitBody<Person>()2.3. 交換
exchangeToMono() メソッドと exchangeToFlux() メソッド(または Kotlin では awaitExchange { } と exchangeToFlow { })は、レスポンスステータスに応じて異なる方法でレスポンスをデコードするなど、より高度な制御が必要な高度なケースに役立ちます。
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});val entity = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Person>()
}
else {
throw response.createExceptionAndAwait()
}
} 上記を使用する場合、返された Mono または Flux が完了した後、レスポンス本体がチェックされ、消費されない場合は解放されて、メモリと接続のリークが防止されます。レスポンスをさらにダウンストリームでデコードすることはできません。必要に応じてレスポンスをデコードする方法を宣言するのは、提供された関数次第です。
2.4. リクエストボディー
リクエスト本体は、次の例に示すように、Mono や Kotlin コルーチン Deferred など、ReactiveAdapterRegistry によって処理される任意の非同期型からエンコードできます。
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);val personDeferred: Deferred<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body<Person>(personDeferred)
.retrieve()
.awaitBody<Unit>()次の例に示すように、オブジェクトのストリームをエンコードすることもできます。
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);val people: Flow<Person> = ...
client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(people)
.retrieve()
.awaitBody<Unit>() または、実際の値がある場合は、次の例に示すように、bodyValue ショートカットメソッドを使用できます。
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);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> の使用方法を示しています。
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);val formData: MultiValueMap<String, String> = ...
client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.awaitBody<Unit>() 次の例に示すように、BodyInserters を使用してインラインでフォームデータを提供することもできます。
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);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, ?> を作成する方法を示しています。
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();val builder = MultipartBodyBuilder().apply {
part("fieldPart", "fieldValue")
part("filePart1", FileSystemResource("...logo.png"))
part("jsonPart", Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build() ほとんどの場合、各パーツに Content-Type を指定する必要はありません。コンテンツ型は、シリアライズするために選択された HttpMessageWriter に基づいて、または Resource の場合はファイル拡張子に基づいて自動的に決定されます。必要に応じて、オーバーロードされたビルダー part メソッドのいずれかを使用して、各パーツに使用する MediaType を明示的に提供できます。
MultiValueMap を準備したら、次の例に示すように、WebClient に渡す最も簡単な方法は body メソッドを使用することです。
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);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 を介して、インラインスタイルのマルチパートコンテンツを提供することもできます。
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);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)を登録できます。
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();val client = WebClient.builder()
.filter { request, next ->
val filtered = ClientRequest.from(request)
.header("foo", "bar")
.build()
next.exchange(filtered)
}
.build()これは、認証などの横断的な関心事に使用できます。次の例では、静的ファクトリ方式による基本認証にフィルターを使用しています。
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication
val client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build() フィルターは、既存の WebClient インスタンスを変更することで追加または削除でき、元のインスタンスに影響を与えない新しい WebClient インスタンスになります。例:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()WebClient は、フィルターのチェーンの周囲の薄いファサードであり、その後に ExchangeFunction が続きます。リクエストを作成したり、上位レベルのオブジェクトとの間でエンコードしたりするためのワークフローを提供し、レスポンスコンテンツが常に消費されるようにできます。フィルターが何らかの方法でレスポンスを処理する場合、常にそのコンテンツを消費するか、そうでなければ同じことを保証する WebClient にダウンストリームで伝播するように特別な注意を払う必要があります。以下は、UNAUTHORIZED ステータスコードを処理するが、予期されるかどうかに関係なく、すべてのレスポンスコンテンツが確実にリリースされるようにするフィルターです。
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}fun renewTokenFilter(): ExchangeFilterFunction? {
return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
next.exchange(request!!).flatMap { response: ClientResponse ->
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return@flatMap response.releaseBody()
.then(renewToken())
.flatMap { token: String? ->
val newRequest = ClientRequest.from(request).build()
next.exchange(newRequest)
}
} else {
return@flatMap Mono.just(response)
}
}
}
}2.6. 属性
リクエストに属性を追加できます。これは、情報をフィルターチェーンに渡し、特定のリクエストに対するフィルターの動作に影響を与える場合に便利です。例:
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);
}val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}
.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.awaitBody<Unit>()defaultRequest コールバックを WebClient.Builder レベルでグローバルに構成できることに注意してください。これにより、すべてのリクエストに属性を挿入できます。これは、たとえば、Spring MVC アプリケーションで ThreadLocal データに基づいてリクエスト属性を設定するために使用できます。
2.7. コンテキスト
属性は、情報をフィルターチェーンに渡す便利な方法を提供しますが、それらは現在のリクエストにのみ影響します。ネストされた追加のリクエストに伝播する情報を渡したい場合。flatMap を介して、または後に実行されます。concatMap 経由の場合は、Reactor Context を使用する必要があります。
Reactor Context は、すべての操作に適用するために、リアクティブチェーンの最後に入力する必要があります。例:
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));2.8. 同期使用
WebClient は、結果の最後でブロックすることにより、同期スタイルで使用できます。
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();val person = runBlocking {
client.get().uri("/person/{id}", i).retrieve()
.awaitBody<Person>()
}
val persons = runBlocking {
client.get().uri("/persons").retrieve()
.bodyToFlow<Person>()
.toList()
}ただし、複数の呼び出しを行う必要がある場合は、各レスポンスを個別にブロックするのを避け、代わりに結合された結果を待つ方が効率的です。
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();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())
}上記は単なる一例です。多くのリモート呼び出しを作成するリアクティブパイプラインをまとめる他のパターンと演算子がたくさんあります。
|
2.9. テスト
WebClient を使用するコードをテストするには、OkHttp MockWebServer [GitHub] (英語) などのモック Web サーバーを使用できます。使用例については、Spring Framework テストスイートの WebClientIntegrationTests [GitHub] (英語) または OkHttp リポジトリの static-server [GitHub] (英語) サンプルを参照してください。
3. WebSocket
リファレンスドキュメントのこのパートでは、リアクティブスタック WebSocket メッセージングのサポートについて説明します。
3.1. WebSocket の概要
WebSocket プロトコルである RFC 6455 [IETF] (英語) は、単一の 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| 1 | Upgrade ヘッダー。 |
| 2 | Upgrade 接続を使用します。 |
通常の 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 を作成できます。次の例は、その方法を示しています。
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) {
// ...
}
}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 にマップできます。
@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);
}
}@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
}WebFlux 構成を使用している場合は、これ以上何もする必要はありません。そうでない場合は、WebFlux 構成を使用していない場合は、以下に示すように WebSocketHandlerAdapter を宣言する必要があります。
@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}@Configuration
class WebConfig {
// ...
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
}3.2.2. WebSocketHandler
WebSocketHandler の handle メソッドは、WebSocketSession を取得して Mono<Void> を返し、セッションのアプリケーション処理が完了したことを示します。セッションは、受信メッセージ用と送信メッセージ用の 2 つのストリームを介して処理されます。次の表に、ストリームを処理する 2 つのメソッドを示します。
WebSocketSession メソッド | 説明 |
|---|---|
| 受信メッセージストリームへのアクセスを提供し、接続が閉じられると完了します。 |
| 送信メッセージのソースを取得し、メッセージを書き込み、ソースが完了して書き込みが完了すると完了する |
WebSocketHandler は、受信ストリームと送信ストリームを統合されたフローに構成し、そのフローの完了を反映する Mono<Void> を返す必要があります。アプリケーション要件に応じて、統合フローは次の場合に完了します。
受信または送信のメッセージストリームが完了します。
受信ストリームは完了します(つまり、接続が閉じられます)が、送信ストリームは無限です。
WebSocketSessionのcloseメソッドを介して選択したポイント。
受信と送信のメッセージストリームが一緒に構成される場合、Reactive Streams は終了アクティビティを通知するため、接続が開いているかどうかを確認する必要はありません。受信ストリームは完了またはエラーシグナルを受信し、送信ストリームはキャンセルシグナルを受信します。
ハンドラーの最も基本的な実装は、受信ストリームを処理するものです。次の例は、このような実装を示しています。
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> を返します。 |
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() を呼び出す必要があります。そうしないと、データを読み取る前にデータバッファが解放される可能性があります。背景については、データバッファとコーデックを参照してください。 |
次の実装では、受信ストリームと送信ストリームを組み合わせます。
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> を返します。 |
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> を返します。 |
次の例に示すように、受信ストリームと送信ストリームは独立しており、完了のためにのみ結合できます。
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> を返します。 |
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 サーバーエンジンに固有の構成を公開します。WebFlux Java 構成を使用する場合は、WebFlux 構成の対応するセクションに示されているようなプロパティをカスタマイズできます。そうでない場合は、WebFlux 構成を使用しない場合は、以下を使用します。
@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);
}
}@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 を実装させ、許可された発信元、ヘッダー、その他の詳細を含む CorsConfiguration を返すことです。それができない場合は、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 メソッドを使用できます。
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());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 モジュールは、ServerHttpRequest、ServerHttpResponse、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_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL または REQUEST_FNF のいずれかを介してリクエストを開始できます。これらの各フレームは、リクエスターからレスポンダーに 1 つのメッセージを運びます。
次に、レスポンダーはレスポンスメッセージとともに PAYLOAD フレームを返します。REQUEST_CHANNEL の場合、リクエスターはさらに多くのリクエストメッセージを含む PAYLOAD フレームを送信します。
リクエストに Request-Stream や Channel などのメッセージのストリームが含まれる場合、レスポンダーはリクエスターからのリクエストシグナルを考慮する必要があります。需要はメッセージの数として表されます。初期需要は、REQUEST_STREAM および REQUEST_CHANNEL フレームで指定されます。後続のリクエストは、REQUEST_N フレームを介して通知されます。
各側は、METADATA_PUSH フレームを介して、個々のリクエストではなく、接続全体に関するメタデータ通知も送信できます。
メッセージフォーマット
RSocket メッセージにはデータとメタデータが含まれます。メタデータは、ルートやセキュリティトークンなどを送信するために使用できます。データとメタデータは異なる形式にすることができます。それぞれの MIME 型は SETUP フレームで宣言され、特定の接続のすべてのリクエストに適用されます。
すべてのメッセージにメタデータを含めることができますが、通常、ルートなどのメタデータはリクエストごとであるため、リクエストの最初のメッセージ、つまりフレーム REQUEST_RESPONSE、REQUEST_STREAM、REQUEST_CHANNEL または REQUEST_FNF の 1 つにのみ含まれます。
プロトコル拡張機能は、アプリケーションで使用する一般的なメタデータ形式を定義します。
複合メタデータ [GitHub] (英語) -- 独立してフォーマットされた複数のメタデータエントリ。
ルーティング [GitHub] (英語) — リクエストのルート。
5.1.2. Java 実装
RSocket の Java 実装 [GitHub] (英語) はプロジェクト Reactor (英語) 上に構築されています。TCP および WebSocket のトランスポートは Reactor Netty [GitHub] (英語) に基づいています。Reactive Streams ライブラリとして、Reactor はプロトコルを実装する作業を簡素化します。アプリケーションでは、宣言演算子と透過的なバックプレッシャーサポートを備えた Flux および Mono を使用するのが自然です。
RSocket Java の API は意図的に最小限かつ基本的なものになっています。これはプロトコルの機能に焦点を当てており、アプリケーションプログラミングモデル (例: RPC codegen とその他) はより高いレベルの独立した関心事として残されています。
メイン契約 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 は、SETUP フレームの接続設定を含む io.rsocket.core.RSocketConnector の準備に役立つビルダーを提供します。
これは、デフォルト設定で接続する最も基本的な方法です。
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);
URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);val requester = RSocketRequester.builder().tcp("localhost", 7000)
URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)上記はすぐには接続されません。リクエストが行われると、共有接続が透過的に確立されて使用されます。
接続設定
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 用のエンコーダーとデコーダーを提供する必要があります。デフォルトでは、String、byte[]、ByteBuffer 用の spring-core の基本コーデックのみが登録されます。spring-web を追加すると、次のように登録できるその他の機能にアクセスできます。
RSocketStrategies strategies = RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
RSocketRequester requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000);val strategies = RSocketStrategies.builder()
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
.rsocketStrategies(strategies)
.tcp("localhost", 7000)RSocketStrategies は再利用のために設計されています。いくつかのシナリオでは、たとえば同じアプリケーションのクライアントとサーバーの場合、Spring 構成で宣言することをお勧めします。
クライアントレスポンダー
RSocketRequester.Builder を使用して、サーバーからのリクエストに対するレスポンダーを構成できます。
サーバーで使用されているものと同じインフラストラクチャに基づいてクライアント側の応答にアノテーション付きハンドラーを使用できますが、次のようにプログラムで登録します。
RSocketStrategies strategies = RSocketStrategies.builder()
.routeMatcher(new PathPatternRouteMatcher()) (1)
.build();
SocketAcceptor responder =
RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(responder)) (3)
.tcp("localhost", 7000);| 1 | spring-web が存在する場合は、効率的なルートマッチングのために PathPatternRouteMatcher を使用します。 |
| 2 | @MessageMapping および / または @ConnectMapping メソッドを使用して、クラスからレスポンダを作成します。 |
| 3 | レスポンダを登録します。 |
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)
.tcp("localhost", 7000)| 1 | spring-web が存在する場合は、効率的なルートマッチングのために PathPatternRouteMatcher を使用します。 |
| 2 | @MessageMapping および / または @ConnectMapping メソッドを使用して、クラスからレスポンダを作成します。 |
| 3 | レスポンダを登録します。 |
上記は、クライアントレスポンダのプログラムによる登録用に設計されたショートカットにすぎないことに注意してください。クライアントレスポンダーが Spring 構成にある代替シナリオの場合、RSocketMessageHandler を Spring Bean として宣言し、次のように適用できます。
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> connector.acceptor(handler.responder()))
.tcp("localhost", 7000);import org.springframework.beans.factory.getBean
val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()
val requester = RSocketRequester.builder()
.rsocketConnector { it.acceptor(handler.responder()) }
.tcp("localhost", 7000) 上記の場合、RSocketMessageHandler で setHandlerPredicate を使用して、クライアントレスポンダーを検出するための別の戦略に切り替える必要がある場合もあります。@RSocketClientResponder などのカスタムアノテーションとデフォルトの @Controller に基づいています。これは、クライアントとサーバー、または同じアプリケーション内の複数のクライアントを使用するシナリオで必要です。
プログラミングモデルの詳細については、アノテーション付きレスポンダーも参照してください。
拡張
RSocketRequesterBuilder は、基になる io.rsocket.core.RSocketConnector を公開するためのコールバックを提供し、キープアライブインターバル、セッション再開、インターセプターなどの詳細設定オプションを提供します。次のように、そのレベルでオプションを構成できます。
RSocketRequester requester = RSocketRequester.builder()
.rsocketConnector(connector -> {
// ...
})
.tcp("localhost", 7000);val requester = RSocketRequester.builder()
.rsocketConnector {
//...
}
.tcp("localhost", 7000)5.2.2. サーバーリクエスター
サーバーから接続されたクライアントにリクエストを行うことは、サーバーから接続されたクライアントのリクエスターを取得することです。
アノテーション付きレスポンダーでは、@ConnectMapping および @MessageMapping メソッドは RSocketRequester 引数をサポートします。これを使用して、接続のリクエスターにアクセスします。@ConnectMapping メソッドは本質的に SETUP フレームのハンドラーであり、リクエストを開始する前に処理する必要があることに注意してください。そのため、最初のリクエストは処理から分離する必要があります。例:
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
requester.route("status").data("5")
.retrieveFlux(StatusReport.class)
.subscribe(bar -> { (1)
// ...
});
return ... (2)
}| 1 | 処理とは無関係に、リクエストを非同期的に開始します。 |
| 2 | 処理を実行し、完了 Mono<Void> を返します。 |
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
GlobalScope.launch {
requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
// ...
}
}
/// ... (2)
}| 1 | 処理とは無関係に、リクエストを非同期的に開始します。 |
| 2 | サスペンド機能で処理してください。 |
5.2.3. 要求
ViewBox viewBox = ... ;
Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
.data(viewBox) (2)
.retrieveFlux(AirportLocation.class); (3)| 1 | リクエストメッセージのメタデータに含めるルートを指定します。 |
| 2 | リクエストメッセージのデータを提供します。 |
| 3 | 予想されるレスポンスを宣言します。 |
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) ステップはオプションです。データを送信しないリクエストの場合はスキップします。
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
.retrieveMono(AirportLocation.class);import org.springframework.messaging.rsocket.retrieveAndAwait
val location = requester.route("find.radar.EWR")
.retrieveAndAwait<AirportLocation>() 複合メタデータ [GitHub] (英語) (デフォルト)を使用し、値が登録済み Encoder でサポートされている場合、追加のメタデータ値を追加できます。例:
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);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 は、メッセージが正常に送信されたことのみを示し、処理されたことを示すものではないことに注意してください。
Metadata-Push の場合、Mono<Void> 戻り値を指定して sendMetadata() メソッドを使用します。
5.3. アノテーション付きレスポンダー
RSocket レスポンダーは、@MessageMapping および @ConnectMapping メソッドとして実装できます。@MessageMapping メソッドは個々のリクエストを処理し、@ConnectMapping メソッドは接続レベルのイベント(セットアップとメタデータプッシュ)を処理します。アノテーション付きのレスポンダーは、サーバー側からの応答とクライアント側からの応答のために対称的にサポートされます。
5.3.1. サーバーレスポンダー
サーバー側でアノテーション付きのレスポンダーを使用するには、RSocketMessageHandler を Spring 構成に追加して、@MessageMapping および @ConnectMapping メソッドで @Controller Bean を検出します。
@Configuration
static class ServerConfig {
@Bean
public RSocketMessageHandler rsocketMessageHandler() {
RSocketMessageHandler handler = new RSocketMessageHandler();
handler.routeMatcher(new PathPatternRouteMatcher());
return handler;
}
}@Configuration
class ServerConfig {
@Bean
fun rsocketMessageHandler() = RSocketMessageHandler().apply {
routeMatcher = PathPatternRouteMatcher()
}
} 次に、Java RSocket API を使用して RSocket サーバーを起動し、レスポンダーの RSocketMessageHandler を次のように接続します。
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);
CloseableChannel server =
RSocketServer.create(handler.responder())
.bind(TcpServerTransport.create("localhost", 7000))
.block();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))
.awaitSingle()RSocketMessageHandler は、デフォルトで複合 [GitHub] (英語) メタデータとルーティング [GitHub] (英語) メタデータをサポートしています。別の MIME 型に切り替えるか、追加のメタデータ MIME 型を登録する必要がある場合は、MetadataExtractor を設定できます。
サポートするメタデータおよびデータ形式に必要な Encoder および Decoder インスタンスを設定する必要があります。コーデックの実装には、spring-web モジュールが必要になる可能性があります。
デフォルトでは、SimpleRouteMatcher は AntPathMatcher を介したルートのマッチングに使用されます。効率的なルートマッチングのために、spring-web から PathPatternRouteMatcher を差し込むことをお勧めします。RSocket ルートは階層化できますが、URL パスではありません。両方のルートマッチャーが "." を使用するように構成されています。デフォルトではセパレータとして使用され、HTTP URL のような URL デコードはありません。
RSocketMessageHandler は、同じプロセスでクライアントとサーバー間で構成を共有する必要がある場合に便利な RSocketStrategies を介して構成できます。
@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();
}
}@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
@Controller
public class RadarsController {
@MessageMapping("locate.radars.within")
public Flux<AirportLocation> radars(MapRequest request) {
// ...
}
}@Controller
class RadarsController {
@MessageMapping("locate.radars.within")
fun radars(request: MapRequest): Flow<AirportLocation> {
// ...
}
} 上記の @MessageMapping メソッドは、ルート "locate.radars.within" を持つ Request-Stream インタラクションに応答します。次のメソッド引数を使用するオプションを備えた柔軟なメソッドシグネチャーをサポートします。
| メソッド引数 | 説明 |
|---|---|
| リクエストのペイロード。これは、 注意 : アノテーションの使用はオプションです。単純型ではなく、サポートされている他の引数のいずれでもないメソッド引数は、予期されるペイロードと見なされます。 |
| リモートの終了をリクエストするためのリクエスター。 |
| マッピングパターンの変数に基づいてルートから抽出された値。 |
| MetadataExtractor で説明されている抽出のために登録されたメタデータ値。 |
| MetadataExtractor に従って、抽出のために登録されたすべてのメタデータ値。 |
戻り値は、レスポンスペイロードとして直列化される 1 つ以上のオブジェクトであると予想されます。これは、Mono または Flux のような非同期型、具体的な値、void または Mono<Void> などの値のない非同期型のいずれかです。
@MessageMapping メソッドがサポートする RSocket インタラクション型は、入力(つまり、@Payload 引数)と出力のカーディナリティから決定されます。カーディナリティは次のことを意味します。
| 基数 | 説明 |
|---|---|
1 | 明示的な値、または |
多 |
|
0 | 入力の場合、これはメソッドに 出力の場合、これは |
以下の表は、すべての入力および出力カーディナリティーの組み合わせと、対応する相互作用型を示しています。
| 入力カーディナリティ | 出力カーディナリティ | インタラクション型 |
|---|---|---|
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 インスタンスを与えて、メタデータをデコードできます。すぐに使用できる「メッセージ /x.rsocket.routing.v0」 [GitHub] (英語) の組み込みサポートがあり、String にデコードして「ルート」キーに保存します。その他の MIME 型の場合は、Decoder を提供して、MIME 型を次のように登録する必要があります。
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");import org.springframework.messaging.rsocket.metadataToExtract
val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo") 複合メタデータは、独立したメタデータ値を組み合わせるのに適しています。ただし、リクエスターは複合メタデータをサポートしていないか、使用しないことを選択する場合があります。このため、DefaultMetadataExtractor は、デコードされた値を出力マップにマップするカスタムロジックを必要とする場合があります。JSON がメタデータに使用される例を次に示します。
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
MimeType.valueOf("application/vnd.myapp.metadata+json"),
new ParameterizedTypeReference<Map<String,String>>() {},
(jsonMap, outputMap) -> {
outputMap.putAll(jsonMap);
});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 に構成済みのデコーダーを使用して抽出プログラムを作成させ、コールバックを使用して登録を次のようにカスタマイズできます。
RSocketStrategies strategies = RSocketStrategies.builder()
.metadataExtractorRegistry(registry -> {
registry.metadataToExtract(fooMimeType, Foo.class, "foo");
// ...
})
.build();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) の助けを借りて行われます。レジストリには、RxJava 3、Kotlin コルーチン、および SmallRye Mutiny のサポートが組み込まれていますが、他のサードパーティ製アダプターも登録できます。
Spring Framework 5.3.11 の時点で、RxJava 1 および 2 のサポートは、RxJava 独自の EOL アドバイスおよび RxJava 3 へのアップグレード推奨に従って非推奨になりました。 |
関数 API(関数エンドポイント、WebClient など)の場合、WebFlux API の一般規則が適用されます。戻り値として Flux および Mono、入力として Reactive Streams Publisher。Publisher がカスタムまたは別のリアクティブライブラリから提供された場合、未知のセマンティクス(0..N)を持つストリームとしてのみ扱うことができます。ただし、セマンティクスがわかっている場合は、生の Publisher を渡す代わりに、Flux または Mono.from(Publisher) でラップすることができます。
例: Mono ではない Publisher を指定すると、Jackson JSON メッセージライターは複数の値を予期します。メディア型が無限ストリーム(application/json+stream など)を意味する場合、値は個別に書き込まれ、フラッシュされます。それ以外の場合、値はリストにバッファリングされ、JSON 配列としてレンダリングされます。