ドキュメントのこのパートでは、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 が作成されたのはなぜですか?
答えの一部は、少ないスレッドで同時実行を処理し、より少ないハードウェアリソースで拡張するために、ノンブロッキング 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、またはその他)を返すことができます。コールごとのレイテンシーまたはコール間の相互依存性が大きいほど、メリットは劇的になります。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 リクエスト処理の基本契約。
WebHandler
API : リクエスト処理用のわずかに高いレベルの汎用 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 (英語) は、プロキシが元のリクエストに関する情報を提供するために使用できる 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 および webflux-cors.html のセクションを参照してください。
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 (英語) 形式にすることができます。
Jackson2Encoder
は次のように機能します。
単一の値のパブリッシャー(例:
Mono
)の場合、単にObjectMapper
を介して直列化します。application/json
を使用する複数値パブリッシャーの場合、デフォルトではFlux#collectToList()
を使用して値を収集し、結果のコレクションを直列化します。application/x-ndjson
やapplication/stream+x-jackson-smile
などのストリーミングメディアタイプを持つ複数値のパブリッシャーの場合、行区切りの JSON (英語) 形式を使用して、各値を個別にエンコード、書き込み、フラッシュします。他のストリーミングメディアタイプは、エンコーダに登録される場合があります。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 名はwebHandler
WebFilter
およびWebExceptionHandler
Beanその他
次の例が示すように、処理はチェーンを構築するために 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 |
|
ビューリゾルバーも参照してください。 |
|
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 個以上のパスセグメントに一致する |
|
| パスセグメントを照合し、「名前」という名前の変数としてキャプチャーする |
|
| 正規表現 |
|
| パスの最後まで 0 個以上のパスセグメントに一致し、「パス」という名前の変数としてキャプチャーする |
|
次の例に示すように、キャプチャーされた 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"
変数は完全な相対パスをキャプチャーします。
構文 {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 パスパターンには、起動時に PropertyPlaceHolderConfigurer
を介してローカル、システム、環境、その他のプロパティソースに対して解決される ${…}
プレースホルダーを埋め込むこともできます。これを使用して、たとえば、外部設定に基づいてベース URL をパラメーター化できます。
Spring WebFlux は、URI パス一致サポートのために PathPattern および PathPatternParser を使用します。両方のクラスは spring-web にあり、実行時に多数の URI パスパターンが一致する Web アプリケーションの HTTP URL パスで使用するために特別に設計されています。 |
Spring WebFlux は、/person
などのマッピングが /person.*
にも一致する Spring MVC とは異なり、サフィックスパターンマッチングをサポートしていません。URL ベースのコンテンツネゴシエーションでは、必要に応じて、クエリパラメーターを使用することをお勧めします。クエリパラメーターは、より単純で、より明示的で、URL パスベースのエクスプロイトに対して脆弱ではありません。
パターン比較
複数のパターンが URL に一致する場合、比較して最適な一致を見つける必要があります。これは、より具体的なパターンを探す PathPattern.SPECIFICITY_COMPARATOR
で行われます。
すべてのパターンについて、URI 変数とワイルドカードの数に基づいてスコアが計算されます。URI 変数のスコアはワイルドカードよりも低くなります。合計スコアの低いパターンが優先されます。2 つのパターンのスコアが同じ場合、長い方が選択されます。
キャッチオールパターン(たとえば、**
、{*varName}
)はスコアリングから除外され、代わりに常に最後にソートされます。2 つのパターンが両方ともキャッチオールである場合、長い方が選択されます。
消費可能なメディアタイプ
次の例に示すように、リクエストの Content-Type
に基づいてリクエストマッピングを絞り込むことができます。
@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 String petId): 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", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | myHeader が myValue と等しいことを確認してください。 |
@GetMapping("/pets", 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 リクエスト本文へのアクセス用。本文コンテンツは、 |
| リクエストヘッダーと本文へのアクセス用。本体は |
|
|
| HTML コントローラーで使用され、ビューレンダリングの一部としてテンプレートに公開されるモデルへのアクセス用。 |
| データバインディングと検証が適用されたモデル内の既存の属性(存在しない場合はインスタンス化)へのアクセス用。
|
| コマンドオブジェクト、つまり |
| フォーム処理の補完をマークするために、クラスレベルの |
| 現在のリクエストのホスト、ポート、スキーム、コンテキストパスに関連する URL を準備するため。URI リンクを参照してください。 |
| 任意のセッション属性へのアクセス — クラスレベルの |
| リクエスト属性へのアクセス用。詳細については、 |
その他の引数 | メソッドの引数が上記のいずれにも一致しない場合、デフォルトでは、BeanUtils#isSimpleProperty (Javadoc) によって決定される単純型の場合は |
戻り値
次の表に、サポートされているコントローラーメソッドの戻り値を示します。このよう Reactor、RxJava、などのライブラリからの反応タイプのことを注意または他の一般的にすべての戻り値のためにサポートされています。
コントローラーメソッドの戻り値 | 説明 |
---|---|
| 戻り値は、 |
| 戻り値は、HTTP ヘッダーを含む完全なレスポンスを指定し、本文は |
| ヘッダーを含み、本文を含まないレスポンスを返すため。 |
|
|
| 暗黙的なモデルと一緒にレンダリングするために使用する |
| 暗黙的なモデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。 |
| モデルに追加される属性。ビュー名はリクエストパスに基づいて暗黙的に決定されます。
|
| モデルおよびビューのレンダリングシナリオ用の API。 |
|
上記のいずれにも当てはまらない場合、 |
| サーバー送信イベントを発行します。 |
その他の戻り値 | 戻り値が上記のいずれにも一致しない場合、デフォルトでは、ビュー名として、 |
型変換
文字列ベースのリクエスト入力を表す一部のアノテーション付きコントローラーメソッド引数(たとえば、@RequestParam
、@RequestHeader
、@PathVariable
、@MatrixVariable
、@CookieValue
)は、引数が String
以外のものとして宣言されている場合、型変換を必要とする場合があります。
このような場合、構成されたコンバーターに基づいて型変換が自動的に適用されます。デフォルトでは、単純型(int
、long
、Date
など)がサポートされています。型変換は、WebDataBinder
(DataBinder
を参照)または Formatters
を FormattingConversionService
に登録(Spring フィールドのフォーマットを参照)することによりカスタマイズできます。
型変換の実際的な課題は、空の文字列ソース値の処理です。このような値は、型変換の結果として null
になった場合、欠落しているものとして扱われます。これは、Long
、UUID
、その他のターゲットタイプに当てはまります。null
の挿入を許可する場合は、引数のアノテーションで required
フラグを使用するか、引数を @Nullable
として宣言します。
行列変数
RFC 3986 (英語) は、パスセグメントの名前と値のペアについて説明します。Spring WebFlux では、Tim Berners-Lee による「古い投稿」 (英語) に基づく「マトリックス変数」と呼ばれますが、URI パスパラメーターとも呼ばれます。
マトリックス変数は任意のパスセグメントに表示でき、各変数はセミコロンで区切られ、複数の値はコンマで区切られます(例: "/cars;color=red,green;year=2012"
)。"color=red;color=green;color=blue"
など、変数名を繰り返し使用して複数の値を指定することもできます。
Spring MVC とは異なり、WebFlux では、URL 内のマトリックス変数の有無はリクエストマッピングに影響しません。つまり、URI コンテンツを使用して変数の内容をマスクする必要はありません。つまり、コントローラーメソッドからマトリックス変数にアクセスする場合は、マトリックス変数が予想されるパスセグメントに URI 変数を追加する必要があります。次の例は、その方法を示しています。
// 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
}
すべてのパスセグメントにマトリックス変数を含めることができるため、次の例に示すように、マトリックス変数がどのパス変数にあると予想されるかを明確にする必要がある場合があります。
// 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
}
次の例に示すように、マトリックス変数をオプションとして定義し、デフォルト値を指定できます。
// 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
}
すべての行列変数を取得するには、次の例に示すように、MultiValueMap
を使用します。
// 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
@RequestParam
アノテーションを使用して、クエリパラメーターをコントローラーのメソッド引数にバインドできます。次のコードスニペットは使用方法を示しています。
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | @RequestParam を使用します。 |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = clinic.loadPet(petId)
model["pet"] = pet
return "petForm"
}
// ...
}
1 | @RequestParam を使用します。 |
サーブレット API の「リクエストパラメーター」の概念は、クエリパラメーター、フォームデータ、マルチパートを 1 つにまとめます。ただし、WebFlux では、それぞれに ServerWebExchange を介して個別にアクセスします。@RequestParam はクエリパラメーターのみにバインドしますが、データバインドを使用して、クエリパラメーター、フォームデータ、マルチパートをコマンドオブジェクトに適用できます。 |
@RequestParam
アノテーションを使用するメソッドパラメーターはデフォルトで必須ですが、@RequestParam
の必須フラグを false
に設定するか、java.util.Optional
ラッパーで引数を宣言することにより、メソッドパラメーターがオプションであることを指定できます。
ターゲットメソッドのパラメータータイプが String
でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。
@RequestParam
アノテーションが Map<String, String>
または MultiValueMap<String, String>
引数で宣言されると、マップにはすべてのクエリパラメーターが入力されます。
@RequestParam
の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。デフォルトでは、(BeanUtils#isSimpleProperty (Javadoc) によって決定される)単純な値型であり、他の引数リゾルバーによって解決されない引数は、@RequestParam
でアノテーションが付けられているかのように扱われます。
@RequestHeader
@RequestHeader
アノテーションを使用して、リクエストヘッダーをコントローラーのメソッド引数にバインドできます。
次の例は、ヘッダー付きのリクエストを示しています。
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
次の例は、Accept-Encoding
および Keep-Alive
ヘッダーの値を取得します。
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | Accept-Encoging ヘッダーの値を取得します。 |
2 | Keep-Alive ヘッダーの値を取得します。 |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | Accept-Encoging ヘッダーの値を取得します。 |
2 | Keep-Alive ヘッダーの値を取得します。 |
ターゲットメソッドのパラメータータイプが String
でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。
@RequestHeader
アノテーションが Map<String, String>
、MultiValueMap<String, String>
、または HttpHeaders
引数で使用される場合、マップにはすべてのヘッダー値が入力されます。
コンマ区切りの文字列を、文字列の配列またはコレクション、または型変換システムに既知の他の型に変換するための組み込みサポートが利用可能です。例: @RequestHeader("Accept") アノテーションが付けられたメソッドパラメーターは、タイプ String である場合がありますが、String[] または List<String> である場合もあります。 |
@CookieValue
@CookieValue
アノテーションを使用して、HTTP Cookie の値をコントローラーのメソッド引数にバインドできます。
次の例は、Cookie を使用したリクエストを示しています。
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
次のコードサンプルは、Cookie 値を取得する方法を示しています。
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 | Cookie 値を取得します。 |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
1 | Cookie 値を取得します。 |
ターゲットメソッドのパラメータータイプが String
でない場合、タイプ変換が自動的に適用されます。型変換を参照してください。
@ModelAttribute
メソッドの引数で @ModelAttribute
アノテーションを使用して、モデルの属性にアクセスしたり、存在しない場合はインスタンス化したりできます。また、モデル属性は、フィールド名に一致する名前を持つクエリパラメーターとフォームフィールドの値でオーバーレイされます。これはデータバインディングと呼ばれ、個々のクエリパラメーターとフォームフィールドの解析と変換に対処する必要がなくなります。次の例では、Pet
のインスタンスをバインドします。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 | Pet のインスタンスをバインドします。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 | Pet のインスタンスをバインドします。 |
上記の例の Pet
インスタンスは、次のように解決されます。
Model
を介してすでに追加されている場合はモデルから。HTTP セッションから
@SessionAttributes
を介して。デフォルトのコンストラクターの呼び出しから。
クエリパラメーターまたはフォームフィールドに一致する引数を持つ「プライマリコンストラクター」の呼び出しから。引数名は、JavaBeans
@ConstructorProperties
またはバイトコード内の実行時保持パラメーター名によって決定されます。
モデル属性インスタンスが取得された後、データバインディングが適用されます。WebExchangeDataBinder
クラスは、クエリパラメーターとフォームフィールドの名前をターゲット Object
のフィールド名に一致させます。一致するフィールドは、必要に応じて型変換が適用された後に入力されます。データバインディング(および検証)の詳細については、検証を参照してください。データバインディングのカスタマイズの詳細については、DataBinder
を参照してください。
データバインディングはエラーになる可能性があります。デフォルトでは、WebExchangeBindException
が発生しますが、コントローラーメソッドでこのようなエラーをチェックするには、次の例に示すように、@ModelAttribute
のすぐ隣に BindingResult
引数を追加できます。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | BindingResult を追加します。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | BindingResult を追加します。 |
javax.validation.Valid
アノテーションまたは Spring の @Validated
アノテーションを追加することにより、データバインディング後に検証を自動的に適用できます(Bean バリデーションおよび Spring 検証も参照)。次の例では、@Valid
アノテーションを使用しています。
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | モデル属性引数で @Valid を使用します。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | モデル属性引数で @Valid を使用します。 |
Spring WebFlux は、Spring MVC とは異なり、モデル内のリアクティブ型をサポートします。たとえば、Mono<Account>
または io.reactivex.Single<Account>
です。@ModelAttribute
引数は、リアクティブ型ラッパーの有無にかかわらず宣言できます。必要に応じて、実際の値に応じて解決されます。ただし、BindingResult
引数を使用するには、前述のように、リアクティブ型ラッパーなしで @ModelAttribute
引数を宣言する必要があることに注意してください。または、次の例に示すように、リアクティブ型を使用してエラーを処理できます。
@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 ->
// ...
}
}
@ModelAttribute
の使用はオプションであることに注意してください。たとえば、その属性を設定するためです。デフォルトでは、単純な値タイプ(BeanUtils#isSimpleProperty (Javadoc) によって決定される)ではなく、他の引数リゾルバーによって解決されない引数は、@ModelAttribute
でアノテーション付けされているものとして扱われます。
@SessionAttributes
@SessionAttributes
は、リクエスト間で WebSession
にモデル属性を保存するために使用されます。これは、特定のコントローラーが使用するセッション属性を宣言する型レベルのアノテーションです。これは通常、後続のアクセスリクエストのためにセッションに透過的に保存されるモデル属性の名前またはモデル属性のタイプをリストします。
次の例を考えてみましょう。
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | @SessionAttributes アノテーションを使用します。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | @SessionAttributes アノテーションを使用します。 |
最初のリクエストで、名前が pet
のモデル属性がモデルに追加されると、自動的に WebSession
に昇格して保存されます。次の例に示すように、別のコントローラーメソッドが SessionStatus
メソッド引数を使用してストレージをクリアするまで、そこに残ります。
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete();
// ...
}
}
}
1 | @SessionAttributes アノテーションを使用します。 |
2 | SessionStatus 変数を使用します。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
if (errors.hasErrors()) {
// ...
}
status.setComplete()
// ...
}
}
1 | @SessionAttributes アノテーションを使用します。 |
2 | SessionStatus 変数を使用します。 |
@SessionAttribute
グローバルに(つまり、フィルターによってコントローラーの外部で)管理される既存のセッション属性にアクセスする必要があり、存在する場合と存在しない場合、メソッドパラメーターで @SessionAttribute
アノテーションを使用できます。次の例を示します。
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | @SessionAttribute を使用します。 |
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
1 | @SessionAttribute を使用します。 |
セッション属性を追加または削除する必要があるユースケースの場合、WebSession
をコントローラーメソッドに挿入することを検討してください。
コントローラーワークフローの一部としてセッションでモデル属性を一時的に保存するには、@SessionAttributes
に従って、SessionAttributes
の使用を検討してください。
@RequestAttribute
@SessionAttribute
と同様に、次の例に示すように、@RequestAttribute
アノテーションを使用して、以前に作成された既存のリクエスト属性にアクセスできます(たとえば、WebFilter
によって)。
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | @RequestAttribute を使用します。 |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | @RequestAttribute を使用します。 |
マルチパートコンテンツ
マルチパートデータで説明したように、ServerWebExchange
はマルチパートコンテンツへのアクセスを提供します。次の例に示すように、コントローラーでファイルアップロードフォーム(ブラウザーなどから)を処理する最良の方法は、コマンドオブジェクトへのデータバインディングを使用することです。
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 {
// ...
}
}
RESTful サービスシナリオで、ブラウザー以外のクライアントからマルチパートリクエストを送信することもできます。次の例では、JSON とともにファイルを使用しています。
POST /someUrl Content-Type: multipart/mixed --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="meta-data" Content-Type: application/json; charset=UTF-8 Content-Transfer-Encoding: 8bit { "name": "value" } --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="file-data"; filename="file.properties" Content-Type: text/xml Content-Transfer-Encoding: 8bit ... File Data ...
次の例に示すように、@RequestPart
を使用して個々のパーツにアクセスできます。
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file) { (2)
// ...
}
1 | @RequestPart を使用してメタデータを取得します。 |
2 | @RequestPart を使用してファイルを取得します。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
}
1 | @RequestPart を使用してメタデータを取得します。 |
2 | @RequestPart を使用してファイルを取得します。 |
次の例に示すように、未加工部分のコンテンツをデシリアライズする(たとえば、@RequestBody
に似た JSON に)ために、Part
の代わりに具体的なターゲット Object
を宣言できます。
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
1 | @RequestPart を使用してメタデータを取得します。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
1 | @RequestPart を使用してメタデータを取得します。 |
@RequestPart
を javax.validation.Valid
または Spring の @Validated
アノテーションと組み合わせて使用できます。これにより、標準 Bean 検証が適用されます。検証エラーにより WebExchangeBindException
が発生し、400(BAD_REQUEST)レスポンスが発生します。例外には、エラーの詳細を含む BindingResult
が含まれます。また、非同期ラッパーで引数を宣言し、エラー関連の演算子を使用することで、コントローラーメソッドで処理することもできます。
@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 {
// ...
}
すべてのマルチパートデータに MultiValueMap
としてアクセスするには、次の例に示すように、@RequestBody
を使用できます。
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
1 | @RequestBody を使用します。 |
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
1 | @RequestBody を使用します。 |
次の例に示すように、ストリーミング形式でマルチパートデータに順次アクセスするには、代わりに @RequestBody
と Flux<Part>
(または Kotlin の Flow<Part>
)を使用できます。
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
// ...
}
1 | @RequestBody を使用します。 |
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
// ...
}
1 | @RequestBody を使用します。 |
@RequestBody
@RequestBody
アノテーションを使用して、リクエスト本体を読み取り、HttpMessageReader を介して Object
にデシリアライズすることができます。次の例では、@RequestBody
引数を使用しています。
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
Spring MVC とは異なり、WebFlux では、@RequestBody
メソッドの引数は、リアクティブ型、完全なノンブロッキング読み取り、および(クライアントからサーバーへの)ストリーミングをサポートします。
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
// ...
}
WebFlux 構成の HTTP メッセージコーデックオプションを使用して、メッセージリーダーを構成またはカスタマイズできます。
@RequestBody
を javax.validation.Valid
または Spring の @Validated
アノテーションと組み合わせて使用できます。これにより、標準 Bean 検証が適用されます。検証エラーにより WebExchangeBindException
が発生し、400(BAD_REQUEST)レスポンスが発生します。例外には、エラーの詳細を含む BindingResult
が含まれており、非同期ラッパーで引数を宣言し、エラー関連の演算子を使用することにより、コントローラーメソッドで処理できます。
@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
は、@RequestBody
を使用する場合とほぼ同じですが、リクエストヘッダーと本文を公開するコンテナーオブジェクトに基づいています。次の例では、HttpEntity
を使用しています。
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
メソッドで @ResponseBody
アノテーションを使用して、HttpMessageWriter を介して戻り値をレスポンス本体に直列化できます。次の例は、その方法を示しています。
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
はクラスレベルでもサポートされます。この場合、すべてのコントローラーメソッドによって継承されます。これは @RestController
の効果であり、これは @Controller
および @ResponseBody
でマークされたメタアノテーションにすぎません。
@ResponseBody
はリアクティブ型をサポートします。つまり、Reactor 型または RxJava 型を返し、それらが生成する非同期値をレスポンスにレンダリングさせることができます。詳細については、ストリーミングおよび JSON レンダリングを参照してください。
@ResponseBody
メソッドを JSON 直列化ビューと組み合わせることができます。詳細については、Jackson JSON を参照してください。
WebFlux 構成の HTTP メッセージコーデックオプションを使用して、メッセージの書き込みを構成またはカスタマイズできます。
ResponseEntity
ResponseEntity
は @ResponseBody
に似ていますが、ステータスとヘッダーがあります。例:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body: String = ...
val etag: String = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
WebFlux は、単一値のリアクティブ型を使用して ResponseEntity
を非同期的に生成すること、および / またはボディの単一値および複数値のリアクティブ型を使用することをサポートします。これにより、次のように ResponseEntity
でさまざまな非同期レスポンスが可能になります。
ResponseEntity<Mono<T>>
またはResponseEntity<Flux<T>>
は、レスポンスステータスとヘッダーをすぐに通知しますが、本文は後の時点で非同期に提供されます。本体が 0..1 値で構成されている場合はMono
を使用し、複数の値を生成できる場合はFlux
を使用します。Mono<ResponseEntity<T>>
は 3 つすべてを提供する — 後の時点で非同期的に、レスポンスステータス、ヘッダー、本文。これにより、非同期リクエスト処理の結果に応じて、レスポンスのステータスとヘッダーを変えることができます。Mono<ResponseEntity<Mono<T>>>
またはMono<ResponseEntity<Flux<T>>>
は、あまり一般的ではありませんが、さらに別の可能性があります。これらは、最初にレスポンスステータスとヘッダーを非同期で提供し、次にレスポンス本体を非同期で提供します。
Jackson JSON
Spring は、Jackson JSON ライブラリのサポートを提供します。
JSON ビュー
Spring WebFlux は、Object
のすべてのフィールドのサブセットのみをレンダリングできる Jackson の直列化ビュー (英語) の組み込みサポートを提供します。@ResponseBody
または ResponseEntity
コントローラーメソッドで使用するには、次の例に示すように、Jackson の @JsonView
アノテーションを使用して直列化ビュークラスをアクティブにします。
@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 ではビュークラスの配列を使用できますが、指定できるのはコントローラーメソッドごとに 1 つだけです。複数のビューをアクティブにする必要がある場合は、複合インターフェースを使用します。 |
1.4.4. Model
@ModelAttribute
アノテーションを使用できます。
モデルからオブジェクトを作成またはアクセスし、
WebDataBinder
を介してリクエストにバインドする@RequestMapping
メソッドのメソッド引数。@Controller
または@ControllerAdvice
クラスのメソッドレベルのアノテーションとして、@RequestMapping
メソッドを呼び出す前にモデルを初期化できます。@RequestMapping
メソッドで、戻り値をモデル属性としてマークします。
このセクションでは、@ModelAttribute
メソッド、または前述のリストの 2 番目の項目について説明します。コントローラーは、@ModelAttribute
メソッドをいくつでも持つことができます。このようなメソッドはすべて、同じコントローラー内の @RequestMapping
メソッドの前に呼び出されます。@ModelAttribute
メソッドは、@ControllerAdvice
を介してコントローラー間で共有することもできます。詳細については、コントローラーのアドバイスのセクションを参照してください。
@ModelAttribute
メソッドには、柔軟なメソッドシグネチャーがあります。これらは、@RequestMapping
メソッドと同じ引数の多くをサポートします(@ModelAttribute
自体とリクエスト本文に関連するものを除く)。
次の例では、@ModelAttribute
メソッドを使用しています。
@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 ...
}
次の例では、1 つの属性のみを追加します。
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number);
}
名前が明示的に指定されていない場合、Conventions (Javadoc) の javadoc に従って、タイプに基づいてデフォルト名が選択されます。オーバーロードされた addAttribute メソッドを使用するか、@ModelAttribute の name 属性(戻り値用)を使用して、明示的な名前をいつでも割り当てることができます。 |
Spring WebFlux は、Spring MVC とは異なり、モデル内のリアクティブ型を明示的にサポートします(たとえば、Mono<Account>
または io.reactivex.Single<Account>
)。次の例に示すように、@ModelAttribute
引数がラッパーなしで宣言されている場合、そのような非同期モデル属性は、@RequestMapping
呼び出しの時点で実際の値に透過的に解決(およびモデル更新)できます。
@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 {
// ...
}
さらに、リアクティブ型ラッパーを持つモデル属性は、ビューのレンダリングの直前に実際の値に解決されます(そしてモデルが更新されます)。
@ModelAttribute
を @RequestMapping
メソッドのメソッドレベルのアノテーションとして使用することもできます。その場合、@RequestMapping
メソッドの戻り値はモデル属性として解釈されます。戻り値がビュー名として解釈される String
でない限り、これは HTML コントローラーのデフォルトの動作であるため、通常は必要ありません。@ModelAttribute
は、次の例に示すように、モデル属性名のカスタマイズにも役立ちます。
@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
または @ControllerAdvice
クラスは、WebDataBinder
のインスタンスを初期化するために、@InitBinder
メソッドを持つことができます。これらは次の目的で使用されます。
リクエストパラメーター(つまり、フォームデータまたはクエリ)をモデルオブジェクトにバインドします。
String
ベースのリクエスト値(リクエストパラメーター、パス変数、ヘッダー、Cookie など)をコントローラーメソッド引数のターゲットタイプに変換します。HTML フォームをレンダリングするときに、モデルオブジェクト値を
String
値としてフォーマットします。
@InitBinder
メソッドは、コントローラー固有の java.beans.PropertyEditor
または Spring Converter
および Formatter
コンポーネントを登録できます。さらに、WebFlux Java 構成を使用して、Converter
および Formatter
タイプをグローバルに共有される FormattingConversionService
に登録できます。
@InitBinder
メソッドは、@ModelAttribute
(コマンドオブジェクト)引数を除き、@RequestMapping
メソッドと同じ引数の多くをサポートします。通常、これらは、登録用の WebDataBinder
引数、および void
戻り値で宣言されます。次の例では、@InitBinder
アノテーションを使用しています。
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
1 | @InitBinder アノテーションを使用します。 |
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
または、共有 FormattingConversionService
を介して Formatter
ベースのセットアップを使用する場合、次の例に示すように、同じアプローチを再利用してコントローラー固有の Formatter
インスタンスを登録できます。
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
}
// ...
}
1 | カスタムフォーマッタ(この場合は DateFormatter )を追加します。 |
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
}
// ...
}
1 | カスタムフォーマッタ(この場合は DateFormatter )を追加します。 |
1.4.6. 例外の管理
@Controller
および @ControllerAdvice クラスには、コントローラーメソッドからの例外を処理する @ExceptionHandler
メソッドを含めることができます。次の例には、このようなハンドラーメソッドが含まれています。
@Controller
public class SimpleController {
// ...
@ExceptionHandler (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
1 | @ExceptionHandler の宣言。 |
@Controller
class SimpleController {
// ...
@ExceptionHandler (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
1 | @ExceptionHandler の宣言。 |
例外は、伝播されるトップレベルの例外(つまり、直接 IOException
がスローされる)、またはトップレベルのラッパー例外内の直接の原因(たとえば、IllegalStateException
内にラップされた IOException
)に一致します。
例外のタイプを一致させるには、前の例に示すように、ターゲットの例外をメソッド引数として宣言することをお勧めします。または、アノテーション宣言により、一致する例外タイプを絞り込むことができます。通常、引数のシグネチャーをできるだけ具体的に指定し、対応する順序で優先順位付けされた @ControllerAdvice
でプライマリルート例外マッピングを宣言することをお勧めします。詳細については、MVC セクションを参照してください。
WebFlux の @ExceptionHandler メソッドは、@RequestMapping メソッドと同じメソッド引数と戻り値をサポートしていますが、リクエスト本体および @ModelAttribute 関連のメソッド引数は例外です。 |
Spring での @ExceptionHandler
メソッドのサポート WebFlux は、@RequestMapping
メソッドの HandlerAdapter
によって提供されます。詳細については、DispatcherHandler
を参照してください。
REST API の例外
REST サービスの一般的な要件は、レスポンスの本文にエラーの詳細を含めることです。Spring Framework は、レスポンス本文のエラー詳細の表現がアプリケーション固有であるため、自動的にそうしません。ただし、@RestController
は、ResponseEntity
戻り値を持つ @ExceptionHandler
メソッドを使用して、レスポンスのステータスと本文を設定できます。そのようなメソッドを @ControllerAdvice
クラスで宣言して、グローバルに適用することもできます。
Spring WebFlux には Spring MVC ResponseEntityExceptionHandler に相当するものがないことに注意してください。これは、WebFlux が ResponseStatusException (またはそのサブクラス)のみを生成し、HTTP ステータスコードに変換する必要がないためです。 |
1.4.7. コントローラーのアドバイス
通常、@ExceptionHandler
、@InitBinder
、@ModelAttribute
メソッドは、それらが宣言されている @Controller
クラス(またはクラス階層)内に適用されます。このようなメソッドを(コントローラー全体で)よりグローバルに適用する場合は、@ControllerAdvice
または @RestControllerAdvice
のアノテーションが付けられたクラスで宣言できます。
@ControllerAdvice
には @Component
のアノテーションが付いています。これは、そのようなクラスをコンポーネントスキャンによって Spring Bean として登録できることを意味します。@RestControllerAdvice
は、@ControllerAdvice
と @ResponseBody
の両方でアノテーションが付けられた合成アノテーションです。これは、本質的に、@ExceptionHandler
メソッドがメッセージ変換(ビューリゾルバーまたはテンプレートのレンダリングに対して)によってレスポンス本文にレンダリングされることを意味します。
起動時に、@RequestMapping
および @ExceptionHandler
メソッドのインフラストラクチャクラスは、@ControllerAdvice
アノテーションが付けられた Spring Bean を検出し、実行時にそれらのメソッドを適用します。グローバル @ExceptionHandler
メソッド(@ControllerAdvice
から)はローカルメソッド(@Controller
から)の後に適用されます。対照的に、グローバル @ModelAttribute
および @InitBinder
メソッドは、ローカルメソッドの前に適用されます。
デフォルトでは、@ControllerAdvice
メソッドはすべてのリクエスト(つまり、すべてのコントローラー)に適用されますが、次の例に示すように、アノテーションの属性を使用してコントローラーのサブセットに絞り込むことができます。
// 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 {}
前の例のセレクターは実行時に評価され、広範囲に使用するとパフォーマンスに悪影響を与える可能性があります。詳細については、@ControllerAdvice
(Javadoc) javadoc を参照してください。
1.5. 関数エンドポイント
Spring WebFlux には、WebFlux.fn が含まれています。WebFlux.fn は、機能を使用してリクエストのルーティングと処理を行い、契約は不変性のために設計されています。これは、アノテーションベースのプログラミングモデルに代わるものですが、それ以外は同じリアクティブコア基盤で実行されます。
1.5.1. 概要
WebFlux.fn では、HTTP リクエストは HandlerFunction
で処理されます。ServerRequest
を受け取り、遅延された ServerResponse
(つまり Mono<ServerResponse>
)を返す関数です。リクエストオブジェクトとレスポンスオブジェクトの両方に、HTTP リクエストとレスポンスへの JDK 8 フレンドリーなアクセスを提供する不変の契約があります。HandlerFunction
は、アノテーションベースのプログラミングモデルの @RequestMapping
メソッドの本体に相当します。
受信リクエストは、RouterFunction
を使用してハンドラー関数にルーティングされます: ServerRequest
を受け取り、遅延 HandlerFunction
(つまり Mono<HandlerFunction>
)を返す関数。ルーター関数が一致すると、ハンドラー関数が返されます。それ以外の場合は、空の Mono。RouterFunction
は @RequestMapping
アノテーションと同等ですが、ルーター関数がデータだけでなく動作も提供するという大きな違いがあります。
RouterFunctions.route()
は、次の例に示すように、ルーターの作成を容易にするルータービルダーを提供します。
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 | Coroutines ルーター DSL を使用してルーターを作成します。Reactive の代替手段も router { } を介して使用できます。 |
RouterFunction
を実行する 1 つの方法は、RouterFunction
を HttpHandler
に変換し、組み込みサーバーアダプターの 1 つを介してインストールすることです。
RouterFunctions.toHttpHandler(RouterFunction)
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
ほとんどのアプリケーションは、WebFlux Java 構成を介して実行できます。サーバーの実行を参照してください。
1.5.2. HandlerFunction
ServerRequest
および ServerResponse
は、HTTP リクエストおよびレスポンスへの JDK 8 フレンドリーなアクセスを提供する不変のインターフェースです。リクエストとレスポンスの両方が、ボディストリームに対する Reactive Streams (英語) バックプレッシャーを提供します。リクエスト本体は、Reactor Flux
または Mono
で表されます。レスポンス本体は、Flux
および Mono
を含む Reactive Streams Publisher
で表されます。詳細については、リアクティブライブラリを参照してください。
ServerRequest
ServerRequest
は HTTP メソッド、URI、ヘッダー、クエリパラメーターへのアクセスを提供し、ボディへのアクセスは body
メソッドを介して提供されます。
次の例では、リクエストの本文を Mono<String>
に抽出します。
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("/person", 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("/person", 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("/person", handler::createPerson))
.build();
val route = coRouter {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
1.5.4. サーバーの実行
HTTP サーバーでルーター関数をどのように実行しますか? 簡単なオプションは、次のいずれかを使用して、ルーター関数を HttpHandler
に変換することです。
RouterFunctions.toHttpHandler(RouterFunction)
RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)
その後、サーバー固有の手順について HttpHandler に従うことにより、返された HttpHandler
を多数のサーバーアダプターで使用できます。
Spring Boot でも使用されるより一般的なオプションは、WebFlux 構成を介して DispatcherHandler
ベースのセットアップで実行することです。WebFlux 構成は、Spring 構成を使用して、リクエストの処理に必要なコンポーネントを宣言します。WebFlux Java 構成は、関数エンドポイントをサポートするために次のインフラストラクチャコンポーネントを宣言します。
RouterFunctionMapping
: Spring 構成内の 1 つ以上のRouterFunction<?>
Bean を検出し、順序付け、RouterFunction.andOther
を介してそれらを結合し、結果として作成されたRouterFunction
にリクエストをルーティングします。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("/person", 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("/person", 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("/person", 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("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
上記の例は、next.handle(ServerRequest)
の呼び出しがオプションであることを示しています。アクセスが許可されている場合にのみハンドラー関数を実行させます。
ルーター関数ビルダーで filter
メソッドを使用する以外に、RouterFunction.filter(HandlerFilterFunction)
を介して既存のルーター関数にフィルターを適用することができます。
関数エンドポイントの CORS サポートは、専用の CorsWebFilter を介して提供されます。 |
1.6. URI リンク
このセクションでは、URI を準備するために Spring Framework で使用可能なさまざまなオプションについて説明します。
1.6.1. UriComponents
Spring MVC および Spring WebFlux
UriComponentsBuilder
は、次の例に示すように、変数を持つ URI テンプレートから URI を作成できます。
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 がどのように機能するかを学ぶために、他の多くの人と一緒にこの記事 を読むか、詳細について仕様を参照してください。
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. @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.4. グローバル構成
細分化されたコントローラーメソッドレベルの構成に加えて、おそらくグローバルな 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.5. 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 (英語) は、使用する前にテンプレートをコンパイルする必要があり、サーバーサイドスクリプトエンジンでは利用できないブラウザー機能をエミュレートするために、ポリフィル (英語) が必要です。次の例は、カスタムレンダリング関数を設定する方法を示しています。
@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 など、同時実行用に設計されていないテンプレートライブラリで非スレッドセーフスクリプトエンジンを使用する場合は、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 (英語) は 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))
}
}
リソースハンドラーは、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 (英語) は、org.webjars:webjars-locator-core
ライブラリがクラスパスに存在するときに自動的に登録される WebJarsResourceResolver
でもサポートされます。リゾルバーは、jar のバージョンを含むように URL を書き換えることができ、バージョンのない受信 URL(たとえば、/jquery/jquery.min.js
から /jquery/1.2.0/jquery.min.js
など)と照合することもできます。
1.11.9. パスマッチング
パスマッチングに関連するオプションをカスタマイズできます。個々のオプションの詳細については、PathMatchConfigurer
(Javadoc) javadoc を参照してください。次の例は、PathMatchConfigurer
の使用方法を示しています。
@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, filterD
val client1 = WebClient.builder()
.filter(filterA).filter(filterB).build()
val client2 = client1.mutate()
.filter(filterC).filter(filterD).build()
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
2.1.1. MaxInMemorySize
コーデックには、アプリケーションのメモリの課題を回避するために、メモリにデータをバッファリングするための制限があります。デフォルトでは、これらは 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 アプリケーション)、globalResources=true
(デフォルト)で ReactorResourceFactory
型の Spring 管理 Bean を宣言して、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(new 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(new ReadTimeoutHandler(10))
.addHandlerLast(new 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(new 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<Object> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else if (response.statusCode().is4xxClientError()) {
// Suppress error status code
return response.bodyToMono(ErrorContainer.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 if (response.statusCode().is4xxClientError) {
return response.awaitBody<ErrorContainer>()
}
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", new FileSystemResource("...logo.png"))
part("jsonPart", new Person("Jason"))
part("myPart", part) // Part from a server request
}
val parts = builder.build()
ほとんどの場合、各パーツに Content-Type
を指定する必要はありません。コンテンツタイプは、シリアライズするために選択された HttpMessageWriter
に基づいて、または Resource
の場合はファイル拡張子に基づいて自動的に決定されます。必要に応じて、オーバーロードされたビルダー part
メソッドのいずれかを使用して、各パーツに使用する MediaType
を明示的に提供できます。
MultiValueMap
を準備したら、次の例に示すように、WebClient
に渡す最も簡単な方法は body
メソッドを使用することです。
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
に影響を与えることなく、フィルターを挿入または削除できます。以下は、インデックス 0 に基本認証フィルターを挿入する例です。
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()
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 (英語) は、単一の 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 vs other など)をより高いレベルの独立した関心事として残しています。
メイン契約 io.rsocket.RSocket: GitHub (英語) は、単一のメッセージの約束を表す Mono
、メッセージのストリームを表す Flux
、およびバイトバッファーとしてデータとメタデータにアクセスする実際のメッセージを表す io.rsocket.Payload
を使用して、4 つのリクエスト対話タイプをモデル化します。RSocket
契約は対称的に使用されます。リクエストの場合、アプリケーションには、リクエストを実行する RSocket
が与えられます。応答のために、アプリケーションは RSocket
を実装してリクエストを処理します。
これは、徹底的な導入を意図したものではありません。ほとんどの場合、Spring アプリケーションはその API を直接使用する必要はありません。ただし、Spring に依存しない RSocket を確認または実験することが重要な場合があります。RSocket Java リポジトリには、API とプロトコルの機能を示す多数のサンプルアプリ: GitHub (英語) が含まれています。
5.1.3. Spring サポート
spring-messaging
モジュールには次のものが含まれます。
RSocketRequester — データとメタデータのエンコード / デコードを使用して
io.rsocket.RSocket
を介してリクエストを行うための流れるような API。アノテーション付きレスポンダー —
@MessageMapping
は、応答用のアノテーション付きハンドラーメソッドです。
spring-web
モジュールには、Jackson CBOR/JSON などの Encoder
および Decoder
実装、および RSocket アプリケーションが必要とする可能性が高い Protobuf が含まれています。また、効率的なルートマッチングのためにプラグインできる PathPatternParser
も含まれています。
Spring Boot 2.2 は、TCP または WebSocket を介した RSocket サーバーの立ち上げをサポートしています。これには、WebFlux サーバーで WebSocket を介して RSocket を公開するオプションも含まれます。RSocketRequester.Builder
および RSocketStrategies
のクライアントサポートと自動構成もあります。詳細については、Spring Boot リファレンスの RSocket セクションを参照してください。
Spring Security 5.2 は RSocket サポートを提供します。
Spring Integration 5.2 は、RSocket クライアントおよびサーバーと対話するための受信および送信ゲートウェイを提供します。詳細については、Spring Integration リファレンスマニュアルを参照してください。
Spring Cloud Gateway は RSocket 接続をサポートしています。
5.2. RSocketRequester
RSocketRequester
は、RSocket リクエストを実行するための流れるような API を提供し、低レベルのデータバッファーではなく、データとメタデータのオブジェクトを受け入れて返します。対称的に使用して、クライアントからリクエストを作成したり、サーバーからリクエストを作成したりできます。
5.2.1. クライアントリクエスター
クライアント側で RSocketRequester
を取得するには、サーバーに接続します。これには、接続設定を含む RSocket SETUP
フレームの送信が含まれます。RSocketRequester
は、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 | @MessageMaping および / または @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 | @MessageMaping および / または @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
インスタンスを与えて、メタデータをデコードできます。すぐに使用できる「message/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 2/3、RxJava 1(RxJava Reactive Streams ブリッジ経由)、および CompletableFuture
のサポートが組み込まれていますが、他のサポートも登録できます。
Spring Framework 5.3 以降、RxJava 1 のサポートは廃止されました。 |
関数 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 配列としてレンダリングされます。