Spring WebFlux 徹底解説!Spring MVCとの違い、メリット、導入方法
インターネットの普及と技術の進化により、Webアプリケーションに求められる要件は年々高度化しています。大量の同時接続を効率的に処理し、低レイテンシで応答性の高いサービスを提供することが、現代のアプリケーション開発における重要な課題となっています。
このような背景から、従来のブロッキングI/Oに基づくフレームワークの限界が見え始め、ノンブロッキングI/Oとリアクティブプログラミングに基づいた新しいアプローチが注目されるようになりました。Spring Frameworkにおいても、この変化に対応するために導入されたのが「Spring WebFlux」です。
本記事では、Spring WebFluxとは何か、従来のSpring MVCとの違い、そのメリットとデメリット、そして具体的な導入方法や開発手法、テスト方法に至るまでを徹底的に解説します。
対象読者
- Spring MVCでの開発経験があり、Spring WebFluxに興味がある方
- リアクティブプログラミングの概念を学び、Springアプリケーションに適用したい方
- 高負荷なI/Oバウンド処理や多数の同時接続を効率的に扱いたい方
- マイクロサービス開発において、適切なフレームワークを選択したい方
1. はじめに:なぜ今、Spring WebFluxなのか?
ウェブアプリケーションは、静的なコンテンツを提供する時代から、動的なインタラクション、リアルタイム通信、そして大量のデータを扱う時代へと進化してきました。スマートフォンの普及、IoTデバイスの増加、マイクロサービスアーキテクチャの採用などにより、サーバー側のアプリケーションはより多くの同時接続と、外部サービスへの頻繁なアクセスを求められています。
従来の多くのWebフレームワーク(Spring MVCを含む)は、JavaのServlet APIに基づいています。Servlet APIは基本的にブロッキングI/Oモデルを採用しており、これはリクエストごとに専用のスレッドを割り当て、そのスレッドがI/O操作(データベースアクセス、外部API呼び出し、ファイル読み書きなど)の完了を待つ間、ブロックされるという仕組みです。
少数の接続であれば問題ありませんが、数千、数万といった大量の同時接続が発生する場合、接続ごとにスレッドを作成・維持すると、メモリやCPUリソースが大量に消費され、スレッド切り替えのオーバーヘッドも増大します。これは、アプリケーションのスケーラビリティを制限する要因となります(いわゆるC10K問題の一側面)。I/O処理がボトルネックとなり、スレッドプールが枯渇すると、新しいリクエストを受け付けられなくなったり、応答速度が著しく低下したりする可能性があります。
このような課題に対処するため、ノンブロッキングI/Oとイベント駆動、そして「Reactive Programming(リアクティブプログラミング)」という考え方が注目されるようになりました。データを「ストリーム」として扱い、そのストリームに対する変更やイベントに「反応(react)」して処理を進めるというこのパラダイムは、特にI/Oバウンドな処理において、少数のスレッドで多数の同時接続を効率的に処理することを可能にします。
Spring Framework 5から導入されたSpring WebFluxは、このリアクティブプログラミングの原則に基づいて構築された、新しいWebフレームワークです。Servlet APIに依存せず、NettyやUndertowといったノンブロッキングサーバー上で動作し、Reactorなどのリアクティブライブラリと連携することで、高いスケーラビリティと効率的なリソース利用を実現します。
2. Spring WebFluxとは?
Spring WebFluxは、Spring Ecosystemの一部であり、ノンブロッキングWebスタックを提供するフレームワークです。Reactorというリアクティブプログラミングライブラリを主要な依存関係として利用し、非同期でノンブロッキングな方法でHTTPリクエストを処理します。
Spring WebFluxは、Reactive Streams仕様(非同期ストリーム処理のための標準仕様)を実装しており、データフローをPublisher(発行者)とSubscriber(購読者)のインタラクションとして扱います。Publisherはデータを発行し、Subscriberはデータを受け取って処理します。このとき、SubscriberはPublisherに対してデータの発行ペースを制御する要求を出すことができ、これによりSubscriberが処理しきれないほどのデータが送られてくることを防ぐ仕組み(Backpressure:バックプレッシャー)が備わっています。
WebFluxは、Spring Web MVCと同様に、DispatcherHandlerを中心とした設計になっています。ただし、DispatcherHandlerが処理するのはHttpServletRequest
とHttpServletResponse
ではなく、Spring Frameworkが定義するリアクティブな抽象化であるServerWebExchange
です。
WebFluxには、Spring MVCライクなアノテーションベースのプログラミングモデル(@Controller
, @RestController
など)と、より関数型・オブジェクト指向なプログラミングモデルであるFunctional Endpoints(RouterFunction
, HandlerFunction
など)の、主に2つの開発スタイルがあります。どちらを選択しても、基盤となるノンブロッキング・リアクティブな処理は共通です。
利用可能なサーバーとしては、デフォルトでNettyが使われますが、UndertowやServlet 3.1+ APIをサポートするTomcat/Jettyなど、ノンブロッキングI/Oをサポートするサーバーであれば利用可能です。
3. なぜSpring WebFluxが必要なのか?Spring MVCとの違い
Spring WebFluxの必要性を理解するためには、従来のSpring MVCとの違いを明確に把握することが不可欠です。両者の違いは、主に基盤となるI/Oモデル、スレッドモデル、そしてそれに伴うプログラミングスタイルにあります。
以下の表に、主要な違いをまとめます。
特徴 | Spring MVC | Spring WebFlux |
---|---|---|
基盤 I/O モデル | ブロッキング I/O | ノンブロッキング I/O |
スレッド モデル | リクエストごとにスレッドを割り当て(ワーカープール) | 少数のイベントループスレッド + 必要に応じてワーカープール |
API スタイル | 命令的 (Imperative) | リアクティブ (Reactive) / 関数型 (Functional) |
スケーラビリティ | スレッド数に依存。CPUバウンド処理に強い。高同時接続でスレッドオーバーヘッド大。 | コネクション数に依存。I/Oバウンド処理に強い。少ないスレッドで高同時接続処理可能。 |
依存ライブラリ | Servlet API | Reactive Streams API, Reactor, Netty/Undertowなど |
ユースケース | CPUバウンド処理が多い、従来のアプリケーション | 高い同時接続が求められるI/Oバウンド処理、マイクロサービス間の連携、WebSockets |
プログラミング モデル | 主にアノテーションベース (@Controller ) |
アノテーションベース (@Controller ) および 関数型 (RouterFunction , HandlerFunction ) |
戻り値の型 | オブジェクト (String, Model, ViewNameなど) | リアクティブ型 (Mono, Flux), オブジェクト |
一つずつ詳細に見ていきましょう。
基盤となるI/Oモデル:ブロッキング vs ノンブロッキング
- Spring MVC (ブロッキング I/O): リクエストが来ると、スレッドが割り当てられます。このスレッドは、データベースへの問い合わせ、外部APIへのリクエスト、ファイル読み書きなどのI/O操作を実行する際、その操作が完了するまで処理を一時停止(ブロック)します。その間、スレッドは他のリクエストを処理することができません。
- Spring WebFlux (ノンブロッキング I/O): リクエストが来ても、I/O操作の完了を待つ間、スレッドはブロックされません。I/O操作を開始したら、スレッドは解放され、他のリクエストの処理や他のI/O操作の開始に移ります。I/O操作が完了すると、OSやランタイムからの通知(イベント)を受け取り、その結果を処理するためのコールバックが実行されます。これにより、少数のスレッドで多数の並行するI/O操作を効率的に扱うことができます。
スレッドモデル:スレッド/リクエスト vs イベントループ
- Spring MVC: 一般的に、Webサーバー(Tomcat, Jettyなど)はスレッドプールを持っており、新しいリクエストが来るたびにプールからスレッドを割り当てます。リクエスト処理が完了するまでそのスレッドを専有するため、同時接続数が増えると、それに比例して多くのスレッドが必要になります。スレッドはOSのリソースであり、無制限に作成することはできません。また、スレッドの切り替え(コンテキストスイッチ)にはオーバーヘッドが発生します。
- Spring WebFlux: NettyやUndertowなどのノンブロッキングサーバーは、通常、少数のイベントループスレッドを使用します。これらのスレッドは、多くの接続からのI/Oイベントを監視し、イベントが発生した際に適切なハンドラーにディスパッチします。CPUバウンドな処理が必要な場合は、別途ワーカープールスレッドを使用することもありますが、I/O待ちでスレッドがブロックされることがないため、必要とされるスレッド数は同時接続数に比べて大幅に少なくなります。
APIスタイル:命令的 vs リアクティブ
-
Spring MVC: コードは上から下へ順に実行される、伝統的な命令型スタイルです。データベースからのデータを取得し、それを処理し、結果を返す、といった一連の処理が順番に記述されます。
java
// Spring MVC (命令的)
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// データベースからユーザーを取得 (ここでスレッドがブロックされる可能性)
User user = userRepository.findById(id);
if (user == null) {
throw new UserNotFoundException();
}
return user;
}
* Spring WebFlux: データフローと非同期イベントに基づいたリアクティブスタイルです。処理は、Publisherが発行するデータをSubscriberが受け取って変換・処理していくパイプラインとして構築されます。コードの実行順序は、データの到着やイベントの発生によって決まります。結果はMono
(0または1つの要素)やFlux
(0からN個の要素)といったリアクティブ型として返されます。java
// Spring WebFlux (リアクティブ)
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
// データベースからユーザーを非同期に取得 (スレッドはブロックされない)
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException())); // 結果が空の場合はエラーを発行
}
リアクティブスタイルは、非同期処理や並列処理を扱うのが得意ですが、命令型に慣れている開発者にとっては学習コストがかかる場合があります。
スケーラビリティ:スレッド数 vs コネクション数
- Spring MVC: スケーラビリティは主にWebサーバーのスレッドプールのサイズによって制限されます。I/Oがボトルネックとなる場合、スレッドを増やしてもI/O待ちが増えるだけで、リソース効率が悪化します。CPU使用率が高く、I/O待ちが少ない(CPUバウンドな)処理が多いアプリケーションには適しています。
- Spring WebFlux: スケーラビリティは、スレッド数よりもコネクション数に対してより効率的です。I/Oバウンドな処理が多いアプリケーション(多数の外部サービスを呼び出すマイクロサービス、リアルタイムデータの送受信など)において、少ないリソースで高い同時接続数を捌くことが得意です。
依存ライブラリ
- Spring MVC: Java Servlet APIに強く依存しています。サーブレットコンテナ(Tomcat, Jetty, WildFlyなど)上で動作します。
- Spring WebFlux: Servlet APIには依存せず、Reactive Streams APIを実装したReactorライブラリが中心です。Netty, Undertow, Jetty (リアクティブモード), Tomcat (リアクティブモード) などの非同期サーバー上で動作します。データベースアクセスに関しても、従来のJDBCドライバ(ブロッキング)ではなく、R2DBC(Reactive Relational Database Connectivity)などのリアクティブなドライバが必要になります。
ユースケース
- Spring MVC: 多くの既存のライブラリやフレームワーク(ORM、セキュリティライブラリ、テンプレートエンジンなど)は、ブロッキングI/Oと命令型プログラミングを前提として設計されています。これらの既存資産を活かしたい場合や、アプリケーションのボトルネックが主にCPUバウンドな処理である場合は、MVCが依然として有力な選択肢です。開発の容易さやエコシステムの成熟度もメリットです。
- Spring WebFlux: 高い同時接続性能が求められるサービス、多くの外部サービスを非同期に呼び出す必要があるマイクロサービス(API Gatewayなど)、リアルタイムデータ処理、WebSocketアプリケーションなどに特に適しています。ただし、連携するライブラリやサービスもリアクティブに対応している必要があります。全ての処理がCPUバウンドである場合は、WebFluxのメリットは少なく、かえって複雑さが増す可能性があります。
どちらのフレームワークを選択するかは、アプリケーションの特性、開発チームの習熟度、既存システムとの連携などを総合的に考慮して判断する必要があります。必ずしもWebFluxがMVCより「優れている」というわけではなく、適した「ユースケースが異なる」という点が重要です。Spring Bootは、spring-boot-starter-web
(MVC) と spring-boot-starter-webflux
を切り替えることで、容易にどちらのスタックを使用するかを選択できます。
4. Reactive Programmingの基本概念
Spring WebFluxを理解するには、リアクティブプログラミングの基本的な考え方を把握しておく必要があります。
Reactive Manifesto
リアクティブシステムは、以下の4つの重要な特性を持つべきだとされる提唱です。
- Responsive(応答性): システムは、時間内に、かつ一貫して応答できる必要があります。これはシステムが使用可能であり、信頼できる基盤を提供することを意味します。
- Resilient(回復力): システムは障害発生時にも応答性を維持する必要があります。レプリケーション、アイソレーション、委譲などのアプローチで実現されます。
- Elastic(弾力性): システムは、ワークロードの変化に応じてリソースの使用を増減させることで応答性を維持する必要があります。
- Message Driven(メッセージ駆動): コンポーネント間で非同期のメッセージを交換することで境界が確立され、アイソレーション、場所の透明性、弾力性の基盤が提供されます。
Spring WebFluxは、特に「Responsive」と「Elastic」なシステムを構築するための基盤を提供します。
Reactive Streams 仕様
Reactive Streamsは、ノンブロッキングなバックプレッシャー付きの非同期ストリーム処理のためのJVMにおける標準仕様です。これは、異なるリアクティブライブラリ(Reactor, RxJavaなど)間の相互運用性を保証するために生まれました。
主要なインターフェースは以下の4つです。
Publisher<T>
: 要素を発行するエンティティ。subscribe(Subscriber<? super T>)
メソッドを持ち、Subscriberはこれを通じて購読を開始します。Subscriber<T>
: Publisherから要素を受け取るエンティティ。以下のメソッドを持ちます。onSubscribe(Subscription s)
: 購読開始時にPublisherからSubscriptionを受け取ります。onNext(T t)
: Publisherから要素を受け取るたびに呼び出されます。onError(Throwable t)
: エラー発生時に呼び出されます。購読は終了します。onComplete()
: 全ての要素の発行が完了したときに呼び出されます。購読は終了します。
Subscription
: SubscriberとPublisher間の購読を表すエンティティ。Subscriberはこれを通じてPublisherに以下の要求を出すことができます。request(long n)
: Publisherにn個の要素を要求します(バックプレッシャー)。cancel()
: 購読をキャンセルします。
Processor<T, R>
: SubscriberとPublisherの両方の性質を持つエンティティ。データを受け取って処理し、結果を別のPublisherとして発行します。
ポイントは、PublisherはSubscriberがrequest(n)
を呼び出すまでデータを送らないという点です。これにより、Subscriberが自分の処理能力を超えないように、データの流れを制御することができます。これがバックプレッシャーです。
Reactor ライブラリ (Mono, Flux)
Spring WebFluxは、Reactive Streams仕様を実装したライブラリの中でも、特にProject Reactorと密接に連携しています。Reactorは、JVM上でノンブロッキングなアプリケーションを構築するためのリアクティブライブラリであり、Spring Ecosystemで広く採用されています。
Reactorの中心となる型は、以下の2つです。
Mono<T>
: 0または1つの要素を発行するPublisherです。非同期に単一の結果(または何もなし、またはエラー)を返す操作(例: IDによるデータ取得、データ保存)に適しています。Flux<T>
: 0からN個の要素を発行するPublisherです。非同期に複数の結果を返す操作(例: 全データ取得、ストリーム処理)に適しています。
MonoとFluxは、データを変換、フィルタリング、結合などを行うための豊富な「オペレーター」を提供します。これらのオペレーターは非同期かつノンブロッキングに動作し、データフローのパイプラインを構築します。
Mono/Flux の例:
- Monoの生成:
java
Mono<String> justMono = Mono.just("Hello"); // "Hello"を発行
Mono<String> emptyMono = Mono.empty(); // 何も発行しない
Mono<String> errorMono = Mono.error(new RuntimeException("Oops")); // エラーを発行 - Fluxの生成:
java
Flux<Integer> justFlux = Flux.just(1, 2, 3, 4, 5); // 1, 2, 3, 4, 5 を順に発行
Flux<String> iterableFlux = Flux.fromIterable(List.of("A", "B", "C")); // リストから発行
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1)); // 1秒おきに数値を無限に発行 -
オペレーターの利用:
“`java
FluxtransformedFlux = justFlux
.filter(n -> n % 2 == 0) // 偶数のみをフィルタリング
.map(n -> n * 2); // 各要素を2倍にするtransformedFlux.subscribe(
data -> System.out.println(“Received: ” + data), // onNext
error -> System.err.println(“Error: ” + error), // onError
() -> System.out.println(“Completed”) // onComplete
);
// 出力例:
// Received: 4
// Received: 8
// Completed
``
filter
この例では、と
mapというオペレーターを使ってデータフローを変換しています。
subscribe()`メソッドが呼び出されたときに、実際にPublisherからSubscriberへのデータの流れが開始されます。この性質を「Cold Publisher」と呼びます(対義語は「Hot Publisher」)。ほとんどのReactorのPublisherはColdです。
リアクティブプログラミングの学習曲線は多少急ですが、非同期処理やデータストリームを扱う上での強力なツールとなります。Spring WebFluxを使用する上で、MonoとFlux、そして主要なオペレーターの使い方を理解することは必須です。
5. Spring WebFluxのメリット・デメリット
Spring WebFluxがどのようなものか、そしてSpring MVCとどう違うのかを理解した上で、そのメリットとデメリットをまとめます。
メリット
- 高いスケーラビリティ (I/Oバウンドなワークロード): 少数のスレッドで多数の同時接続を効率的に処理できるため、I/O待ち時間が長いアプリケーション(データベースアクセス、外部API呼び出しが多い)において、従来のブロッキングモデルよりも高いスループットと低レイテンシを実現できます。特に、マイクロサービス間の連携やAPI Gatewayなど、多くの外部サービスをオーケストレーションするような場合に有効です。
- 効率的なリソース利用: 多数のスレッドを維持する必要がないため、メモリ消費やCPUリソースの使用を抑えることができます。これにより、同一ハードウェア上でより多くの接続を処理できるようになります。
- 回復力 (Backpressure): Reactive Streams仕様に基づくバックプレッシャー機構により、PublisherがSubscriberの処理能力を超えてデータを送りつけることを防ぐことができます。これにより、システム全体の安定性を向上させることができます。
- ノンブロッキングエコシステムとの連携: Reactive Streamsに対応したライブラリやデータベースドライバ(R2DBCなど)と自然に連携できます。
- モダンなプログラミングスタイル: 関数型プログラミングを取り入れたFunctional Endpointsは、モジュール性やテスト容易性を高める可能性があります。
デメリット
- 学習コストが高い: リアクティブプログラミングパラダイムは、従来の命令型プログラミングとは思考様式が大きく異なります。Mono/Fluxや各種オペレーター、非同期データフローのデバッグ方法などを習得するには時間と労力が必要です。
- デバッグが難しい: 非同期かつイベント駆動であるため、従来のステップ実行によるデバッグが難しい場合があります。スタックトレースも追いにくくなることがあります。Reactorはデバッグを支援する機能(例:
checkpoint()
,log()
オペレーター)を提供していますが、それでも同期コードよりは困難になる傾向があります。 - 既存のブロッキングライブラリとの連携の難しさ: JDBCベースのデータベースドライバ、多くのHTTPクライアントライブラリ、一部の認証・認可ライブラリなど、従来のJavaエコシステムにはブロッキングI/Oを前提としたものが多数存在します。これらのライブラリをWebFluxアプリケーションで使用するには、適切なアダプター(例:
Schedulers.boundedElastic()
を使ってブロッキング処理を別スレッドプールで実行するなど)が必要になり、複雑さが増す可能性があります。 - CPUバウンドな処理には不向き: アプリケーションのボトルネックが主にCPU演算である場合、WebFluxのノンブロッキングI/Oのメリットは限定的です。むしろ、リアクティブプログラミングのオーバーヘッドや複雑さがデメリットとなる可能性があります。CPUバウンドな処理は、リアクティブストリーム内で直接実行すると、イベントループスレッドをブロックしてしまうため、専用のスケジューラーで実行する必要があります。
- エコシステムの成熟度: Spring MVCと比較すると、対応しているライブラリやコミュニティでの情報量はまだ少ない場合があります(ただし、急速に改善されています)。
これらのメリットとデメリットを考慮し、WebFluxが解決したい問題(高同時接続、I/Oスループット)に合致する場合に採用を検討するのが賢明です。
6. Spring WebFluxの導入方法
Spring WebFluxプロジェクトを始めるには、Spring Bootのスタータープロジェクトを利用するのが最も簡単です。
Spring Initializr (start.spring.io) を使うか、ビルドツール(GradleまたはMaven)の設定ファイルを直接編集します。
Spring Initializr を使用する場合
- Spring InitializrのWebサイト (https://start.spring.io/) にアクセスします。
- プロジェクト設定(Maven/Gradle, Javaバージョン, Spring Bootバージョンなど)を選択します。
-
Dependencies (依存関係) に、以下の項目を追加します。
- Spring Reactive Web (spring-boot-starter-webflux): これがWebFluxのコアスターターです。NettyなどのリアクティブサーバーやReactorライブラリ、Spring WebFluxのフレームワーク部分が含まれます。
- 必要に応じて、データベースや他のライブラリの依存関係も追加します。例えば、リアクティブなデータベースアクセスが必要な場合は、Spring Data R2DBCと対応するデータベースドライバ(H2, PostgreSQL, MySQLなど)を追加します。
-
「Generate」ボタンをクリックしてプロジェクトをダウンロードし、IDEにインポートすれば準備完了です。
build.gradle または pom.xml を手動で編集する場合
既存のSpring BootプロジェクトにWebFluxを追加する場合や、Initializrを使わない場合は、ビルドツール設定ファイルを編集します。
Gradle (Groovy DSL):
build.gradle
ファイルの dependencies
ブロックに以下を追加します。
“`gradle
dependencies {
// Spring Boot WebFlux Starter (includes Netty by default)
implementation ‘org.springframework.boot:spring-boot-starter-webflux’
// Example: Add R2DBC and H2 database for reactive data access
// implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
// runtimeOnly 'io.r2dbc:r2dbc-h2'
// Optional: Reactor test dependencies
testImplementation 'io.projectreactor:reactor-test'
// Spring Boot Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
“`
Gradle (Kotlin DSL):
build.gradle.kts
ファイルの dependencies
ブロックに以下を追加します。
“`kotlin
dependencies {
// Spring Boot WebFlux Starter (includes Netty by default)
implementation(“org.springframework.boot:spring-boot-starter-webflux”)
// Example: Add R2DBC and H2 database for reactive data access
// implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
// runtimeOnly("io.r2dbc:r2dbc-h2")
// Optional: Reactor test dependencies
testImplementation("io.projectreactor:reactor-test")
// Spring Boot Test
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
“`
Maven:
pom.xml
ファイルの <dependencies>
ブロックに以下を追加します。
“`xml
<!-- Example: Add R2DBC and H2 database for reactive data access -->
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>runtime</scope>
</dependency>
-->
<!-- Optional: Reactor test dependencies -->
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
“`
依存関係を追加したら、ビルドツールでプロジェクトをリフレッシュまたはビルドしてください。
アプリケーション設定
特別なWebFlux固有の設定はほとんど必要ありません。Spring Bootが自動的にspring-boot-starter-webflux
を検出し、WebFluxスタックを構成します。
使用するサーバー(Netty, Undertowなど)やポート番号は、application.properties
または application.yml
でSpring Bootの標準設定を使って行います。
“`properties
application.properties
server.port=8080
server.reactive.platform=netty # デフォルト
“`
7. Spring WebFluxのプログラミングモデル
Spring WebFluxは、開発者に2つの主要なプログラミングモデルを提供します。
- Annotation-based Controllers: Spring MVCで慣れ親しんだ
@Controller
や@RestController
を使ったアノテーション駆動のスタイルです。リアクティブな型(Mono, Flux)を戻り値や引数に使用することで、WebFluxに対応します。 - Functional Endpoints: ルーティング (
RouterFunction
) とリクエストハンドリング (HandlerFunction
) を分離した関数型スタイルです。より軽量で、テスト容易性が高いとされます。
どちらのモデルも、基盤となるWebFluxランタイム(DispatcherHandlerなど)上で動作します。プロジェクトの性質やチームの好みに応じて選択できます。両者を混在させることも可能です。
Annotation-based Controllers
Spring MVCに慣れている開発者にとって最も移行しやすいスタイルです。コントローラークラスに@RestController
アノテーションを付け、ハンドラーメソッドに@RequestMapping
や@GetMapping
などのアノテーションを付けます。
WebFluxでは、これらのハンドラーメソッドの戻り値として、Spring MVCで使われるString
(ビュー名)やPOJO、ResponseEntity
などに加えて、Mono<?>
や Flux<?>
を使用できます。
- 単一のリソースを返す場合や、非同期処理の結果を待ってからレスポンスを返したい場合は
Mono<?>
を戻り値にします。 - 複数のリソースをストリームとして返す場合(例: SSE – Server-Sent Events)は
Flux<?>
を戻り値にします。 - リクエストボディを受け取る
@RequestBody
や、パス変数・リクエストパラメータを表す@PathVariable
,@RequestParam
なども、リアクティブ型を受け取ることができます(例:@RequestBody Mono<User>
)。
例:
“`java
package com.example.webfluxdemo.controller;
import com.example.webfluxdemo.model.User;
import com.example.webfluxdemo.repository.UserRepository;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping(“/api/users”)
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public Flux<User> getAllUsers() {
// データベースから全てのユーザーを非同期に取得し、Fluxとして返す
return userRepository.findAll();
}
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable Long id) {
// データベースから指定されたIDのユーザーを非同期に取得し、Monoとして返す
return userRepository.findById(id);
}
@PostMapping
public Mono<User> createUser(@RequestBody User user) {
// 受け取ったユーザー情報を非同期に保存し、保存されたユーザーを返す
return userRepository.save(user);
}
// 他のCRUD操作なども同様に実装...
}
“`
この例では、Spring Data R2DBCのUserRepository
を使用しています。findAll()
はFlux<User>
を、findById(id)
はMono<User>
を、save(user)
はMono<User>
を返します。コントローラーメソッドがこれらのリアクティブ型をそのまま返すことで、WebFluxランタイムがストリームの完了を待ち、結果をHTTPレスポンスとして適切にシリアライズして返します。
Functional Endpoints (RouterFunction & HandlerFunction)
Functional Endpointsは、より関数型プログラミングに近いスタイルでWebエンドポイントを定義します。ルーティングルールとリクエスト処理ロジックが明確に分離されます。
HandlerFunction<T>
: HTTPリクエストを処理する関数です。ServerRequest
を受け取り、Mono<ServerResponse>
を返します。ビジネスロジックやデータアクセスはこの中で実行されます。これは従来のコントローラーメソッドやリクエストハンドラーに相当します。RouterFunction<T>
: incoming リクエストを受け取り、それに対応するHandlerFunction
を返す関数です。リクエストパス、HTTPメソッド、ヘッダーなどの条件に基づいてルーティングを定義します。これは従来の@RequestMapping
アノテーションやXML設定に相当します。
通常、ルーティングは@Configuration
クラス内でBeanとして定義します。RouterFunctions.route()
メソッドを使って、リクエスト述語(RequestPredicates
)とHandlerFunctionを関連付けます。
例:
まず、リクエストを処理する HandlerFunction
を持つクラス(UserHandler
)を作成します。
“`java
package com.example.webfluxdemo.handler;
import com.example.webfluxdemo.model.User;
import com.example.webfluxdemo.repository.UserRepository;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component // Beanとして登録
public class UserHandler {
private final UserRepository userRepository;
public UserHandler(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
// 全ユーザーを取得するFluxを取得し、OKステータスでJSONとして返す
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(userRepository.findAll(), User.class);
}
public Mono<ServerResponse> getUserById(ServerRequest request) {
Long userId = Long.valueOf(request.pathVariable("id"));
// 指定IDのユーザーを取得し、存在すればOKステータスでJSONとして返す
// 存在しない場合はNOT_FOUNDを返す
return userRepository.findById(userId)
.flatMap(user -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> createUser(ServerRequest request) {
// リクエストボディからUserオブジェクトを取得し、保存する
Mono<User> user = request.bodyToMono(User.class);
return user.flatMap(u -> ServerResponse.status(201) // 201 Created
.contentType(MediaType.APPLICATION_JSON)
.body(userRepository.save(u), User.class));
}
// 他のCRUD操作なども同様にHandlerFunctionとして実装...
}
“`
次に、ルーティングを定義する @Configuration
クラスを作成します。
“`java
package com.example.webfluxdemo.router;
import com.example.webfluxdemo.handler.UserHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
@Configuration(proxyBeanMethods = false) // Lite モードで高速化
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> routeUser(UserHandler userHandler) {
return RouterFunctions.route(GET("/api/functional/users").and(accept(MediaType.APPLICATION_JSON)), userHandler::getAllUsers)
.andRoute(GET("/api/functional/users/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUserById)
.andRoute(POST("/api/functional/users").and(contentType(MediaType.APPLICATION_JSON)), userHandler::createUser);
// 他のCRUD操作のルーティングも追加...
}
}
“`
RouterFunctions.route()
メソッドを使って、特定のリクエスト(RequestPredicates
で定義、例: GET("/api/functional/users")
)と、対応するHandlerFunction
(userHandler::getAllUsers
)を結びつけています。
Functional Endpointsは、DI(依存性注入)を利用しつつ、ルーティングとハンドリングロジックを明確に分離できるため、より柔軟でテストしやすい構造になります。特に、小さなマイクロサービスや、API Gatewayのような特定の用途に特化したアプリケーションに適していると言われます。
どちらのスタイルもWebFluxのリアクティブな能力をフルに活用できます。Annotation-based ControllerはSpring MVCからの移行が容易で、大人数チームや複雑なアプリケーションに適しているかもしれません。Functional Endpointsはより関数型スタイルを好み、柔軟性やテスト容易性を重視する場合に適しているかもしれません。
8. Spring WebFluxを使った簡単なアプリケーション開発(CRUD REST API例)
ここでは、前述のプログラミングモデルを利用して、簡単なユーザー管理のCRUD(Create, Read, Update, Delete)REST APIを実装する例を示します。データ永続化にはSpring Data R2DBCとH2データベース(インメモリモード)を使用します。
プロジェクトには以下の依存関係が必要です(spring-boot-starter-webflux
とテスト関連に加えて):
gradle
// build.gradle (一部抜粋)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
runtimeOnly 'io.r2dbc:r2dbc-h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
1. エンティティの定義
R2DBCを使用するため、データベーステーブルに対応するエンティティクラスを定義します。
“`java
package com.example.webfluxdemo.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
@Table(“users”) // テーブル名を指定
public class User {
@Id // 主キーを指定
private Long id;
private String name;
private String email;
// デフォルトコンストラクタ (R2DBCのために必要)
public User() {}
public User(String name, String email) {
this.name = name;
this.email = email;
}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters and Setters (または Lombok)
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
'}';
}
}
“`
2. Reactive Repository の定義
Spring Data R2DBCを利用して、データベース操作のためのリアクティブなリポジトリを定義します。
“`java
package com.example.webfluxdemo.repository;
import com.example.webfluxdemo.model.User;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository // Beanとして登録
public interface UserRepository extends ReactiveCrudRepository
// ReactiveCrudRepository が基本的な CRUD 操作 (findAll, findById, save, deleteByIdなど) をMono/Fluxを返す形で提供
// 必要に応じてカスタムクエリメソッドを追加可能
// Mono
}
“`
ReactiveCrudRepository
を継承するだけで、Spring Data R2DBCがリアクティブなCRUD操作の実装を自動生成してくれます。
3. R2DBC 設定
H2インメモリデータベースを使用するための設定をapplication.properties
に記述します。また、アプリケーション起動時にテーブルを作成するためのSQLスクリプトを指定します。
“`properties
application.properties
spring.r2dbc.url=r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1
spring.r2dbc.username=sa
spring.r2dbc.password=password
R2DBC schema initialization
spring.r2dbc.initialize=true
spring.r2dbc.schema=classpath:schema.sql
“`
schema.sql
ファイルはsrc/main/resources
ディレクトリに作成します。
“`sql
— src/main/resources/schema.sql
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
“`
4. CRUD API 実装 (Annotation-based Controller スタイル)
Annotation-based ControllerスタイルでCRUD APIを実装します。
“`java
package com.example.webfluxdemo.controller;
import com.example.webfluxdemo.model.User;
import com.example.webfluxdemo.repository.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping(“/api/users”)
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
public Flux<User> getAllUsers() {
return userRepository.findAll(); // Flux<User>を返す
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long id) {
return userRepository.findById(id) // Mono<User>を返す
.map(user -> ResponseEntity.ok(user)) // ユーザーが見つかれば200 OKで返す
.defaultIfEmpty(ResponseEntity.notFound().build()); // 見つからなければ404 Not Foundを返す
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED) // 201 Created ステータスを返す
public Mono<User> createUser(@RequestBody User user) {
// saveはMono<User>を返す。新しいユーザーのidが設定される。
return userRepository.save(user);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<User>> updateUser(@PathVariable Long id, @RequestBody User user) {
return userRepository.findById(id) // 既存ユーザーを取得
.flatMap(existingUser -> {
// 既存ユーザーを更新
existingUser.setName(user.getName());
existingUser.setEmail(user.getEmail());
// 更新したユーザーを保存 (Mono<User>を返す)
return userRepository.save(existingUser);
})
.map(updatedUser -> ResponseEntity.ok(updatedUser)) // 更新成功で200 OKを返す
.defaultIfEmpty(ResponseEntity.notFound().build()); // 既存ユーザーが見つからなければ404 Not Foundを返す
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) // 204 No Content ステータスを返す
public Mono<Void> deleteUser(@PathVariable Long id) {
// deleteByIdは処理完了を示すMono<Void>を返す
return userRepository.deleteById(id);
}
}
“`
各メソッドの戻り値がMono
またはFlux
になっている点、およびResponseEntity
を使用してHTTPステータスやヘッダーを細かく制御している点に注目してください。
アプリケーションクラス
Spring Bootアプリケーションのメインクラスは標準的なもので構いません。
“`java
package com.example.webfluxdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication
@EnableR2dbcRepositories // R2DBCリポジトリを有効化
public class WebfluxDemoApplication {
public static void main(String[] args) {
SpringApplication.run(WebfluxDemoApplication.class, args);
}
}
“`
@EnableR2dbcRepositories
を付けて、Spring Data R2DBCリポジトリを有効にする必要があります。
アプリケーションを起動すると、http://localhost:8080/api/users
でAPIエンドポイントにアクセスできるようになります。
9. WebClientを使った外部API呼び出し
WebFluxには、ノンブロッキングかつリアクティブなHTTPクライアントであるWebClient
が用意されています。これは、Spring MVCでよく使われるブロッキングなRestTemplate
の代替となるもので、特に外部サービスへの非同期呼び出しが多いアプリケーション(例: マイクロサービスが他のマイクロサービスを呼び出す場合)で威力を発揮します。
WebClient
は、デフォルトでNetty上で動作し、Reactor型(Mono, Flux)を扱います。
基本的な使い方は以下のようになります。
“`java
package com.example.webfluxdemo.client;
import com.example.webfluxdemo.model.User; // 外部APIが返すオブジェクト型を想定
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Component
public class ExternalApiClient {
private final WebClient webClient;
public ExternalApiClient(WebClient.Builder webClientBuilder) {
// WebClient.Builder をインジェクトして、カスタマイズ可能なWebClientを生成
this.webClient = webClientBuilder.baseUrl("http://external-api-url").build();
// もしくは WebClient.create("http://external-api-url");
}
public Flux<User> getAllExternalUsers() {
return webClient.get() // GETリクエスト
.uri("/users") // URI指定
.retrieve() // リクエスト実行し、レスポンスを取得
.bodyToFlux(User.class); // レスポンスボディをUserのFluxとして取得
}
public Mono<User> getExternalUserById(String id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.bodyToMono(User.class); // レスポンスボディをUserのMonoとして取得
}
public Mono<User> createExternalUser(User user) {
return webClient.post() // POSTリクエスト
.uri("/users")
.bodyValue(user) // リクエストボディにUserオブジェクトを設定
.retrieve()
.bodyToMono(User.class);
}
// エラーレスポンスのハンドリング例
public Mono<User> getExternalUserWithErrorHandler(String id) {
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(status -> status.is4xxClientError(), // 4xxクライアントエラーの場合
response -> Mono.error(new RuntimeException("Client error: " + response.statusCode()))) // 例外をMonoとして発行
.onStatus(status -> status.is5xxServerError(), // 5xxサーバーエラーの場合
response -> Mono.error(new RuntimeException("Server error: " + response.statusCode())))
.bodyToMono(User.class);
}
}
“`
WebClient.Builder
をインジェクトして利用するのが推奨される方法です。ベースURLやデフォルトヘッダーなどを設定した共通のBuilderをBeanとして定義しておくと便利です。
retrieve()
メソッドは、レスポンスを取得する基本的な方法です。ステータスコードが4xxや5xxの場合は例外を投げます(デフォルト)。onStatus()
メソッドを使って、特定のステータスコードに対するカスタム処理(例: 特定のエラークラスを投げる)を定義することもできます。
bodyToMono(Class)
やbodyToFlux(Class)
を使って、レスポンスボディを指定した型のMonoまたはFluxとして取得します。
WebClient
は、WebFluxアプリケーション内だけでなく、WebFluxを使用していないアプリケーションからでも(依存関係を追加すれば)単独で利用可能です。
10. エラーハンドリング
リアクティブストリームにおけるエラーは、例外としてスローされるのではなく、onError
シグナルとしてストリームを流れます。このシグナルを受け取ったオペレーターやSubscriberは、エラーを処理するか、そのまま下流に伝播させます。エラーシグナルはストリームを終了させるため、エラーが発生したストリームからはそれ以降onNext
やonComplete
シグナルは発行されません。
Spring WebFluxでは、このリアクティブストリームのエラー伝播の仕組みを利用してエラーを扱います。HTTPレベルのエラー(例: 404 Not Found, 500 Internal Server Error)は、適切なServerResponse
として返されます。
Operatorを使ったエラーハンドリング
MonoやFluxのオペレーターチェーンの中で発生したエラーは、様々なオペレーターを使って処理できます。
onErrorResume(Function<? super Throwable, ? extends Publisher<? extends T>>)
: エラーが発生した場合に、代替となるPublisherに処理を切り替えます。例えば、フォールバックデータを提供する場合に使います。
java
someMono.onErrorResume(error -> {
if (error instanceof SpecificException) {
return Mono.just(defaultValue); // 特定のエラーならデフォルト値を返すMonoを返す
}
return Mono.error(error); // それ以外のエラーはそのまま伝える
});onErrorReturn(T fallbackValue)
: エラーが発生した場合に、指定されたデフォルト値を返すMono/Fluxに切り替えます。
java
someFlux.onErrorReturn(defaultValue); // どんなエラーでもデフォルト値一つを返すdoOnError(Consumer<? super Throwable>)
: エラーシグナルを受け取った際に、副作用として何らかの処理(ロギングなど)を実行しますが、エラー自体は下流に伝播させます。
java
someMono.doOnError(error -> log.error("Error occurred: {}", error.getMessage()));
これらのオペレーターは、特定のデータフロー内でのエラーリカバリやロギングに役立ちます。
グローバルエラーハンドリング
WebFluxアプリケーション全体で発生するエラーを共通で処理するには、WebExceptionHandler
インターフェースを実装したBeanを定義します。これはSpring MVCにおける@ControllerAdvice
と似ていますが、より低レベルで動作します。
WebExceptionHandler
は、handle(ServerWebExchange exchange, Throwable ex)
メソッドを持ち、ServerWebExchange
(リクエストとレスポンスの情報を含む)と発生したThrowable
を受け取り、エラーレスポンスを書き込むMono<Void>
を返します。
Spring Bootは、デフォルトでDefaultErrorWebExceptionHandler
を提供しており、これは基本的なエラーページやJSONレスポンスを生成します。これをカスタマイズするか、独自のWebExceptionHandler
を実装してSpringコンテキストに登録することで、グローバルなエラーハンドリングを実現できます。
“`java
package com.example.webfluxdemo.config;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.server.*;
import reactor.core.publisher.Mono;
import java.util.Map;
// WebFluxのエラーハンドリングをカスタマイズする例
// 通常はDefaultErrorWebExceptionHandlerをカスタマイズするか、よりシンプルな方法を使うことも多い
// この例は関数型エンドポイントと組み合わせて使うカスタムWebExceptionHandlerの骨格
/*
@Configuration
@Order(-1) // 標準のDefaultErrorWebExceptionHandlerより高い優先度
public class GlobalErrorHandlingConfig {
private final ErrorAttributes errorAttributes;
private final ResourceProperties resourceProperties;
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
public GlobalErrorHandlingConfig(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties,
ServerProperties serverProperties,
ApplicationContext applicationContext) {
this.errorAttributes = errorAttributes;
this.resourceProperties = resourceProperties;
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
}
@Bean
public WebExceptionHandler webExceptionHandler() {
// DefaultErrorWebExceptionHandlerをベースにカスタマイズする
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(
errorAttributes, resourceProperties, serverProperties.getError(), applicationContext);
exceptionHandler.setMessageWriters(ServerCodecConfigurer.create().getWriters());
exceptionHandler.setMessageReaders(ServerCodecConfigurer.create().getReaders());
// カスタムレンダリングやエラー情報の加工などを追加
// exceptionHandler.setRenderExceptionHandler((exchange, ex) -> { ... });
return exceptionHandler;
}
// カスタムのエラー属性を追加する場合
@Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Map<String, Object> errorAttributes = super.getErrorAttributes(request, includeStackTrace);
// ここでerrorAttributesにカスタム情報を追加できる
// errorAttributes.put("custom_message", "This is a custom error attribute");
return errorAttributes;
}
};
}
}
*/
// より簡単な方法としては、@ControllerAdvice + @ExceptionHandler が Annotation-based Controllers で利用可能。
// あるいは、ErrorWebExceptionHandler を直接実装する方法もある。
// 以下は、特定の例外に対するシンプルな ErrorWebExceptionHandler の例(完全ではない)
/*
@Component
@Order(-2) // デフォルトより優先度高く
public class SpecificExceptionHandler implements WebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (ex instanceof ResourceNotFoundException) {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
// レスポンスボディにエラー情報を書き込む処理をここに追加
// return exchange.getResponse().writeWith(...);
return Mono.empty(); // 処理完了
}
// 他の例外はデフォルトのハンドラーに任せる
return Mono.error(ex);
}
}
*/
``
WebExceptionHandler
関数型エンドポイントの場合は、アノテーションベースの場合は
@ControllerAdviceと
@ExceptionHandlerアノテーションを組み合わせるのが一般的なアプローチです。ただし、
@ControllerAdviceもWebFluxに対応しており、
Mono/
Flux`を返すハンドラーメソッドを定義できます。
エラーハンドリングは、リアクティブプログラミングの複雑さが増す部分の一つです。エラーがストリームのどの段階で発生し、どのように下流に伝播するかを理解することが重要です。
11. WebFluxアプリケーションのテスト
Spring WebFluxアプリケーションのテストには、専用のテストクライアントであるWebTestClient
を使用するのが便利です。WebTestClient
は、インメモリでWebFluxアプリケーションを起動し、HTTPリクエストを送信してそのレスポンスを検証することができます。実際のネットワーク通信を行わないため、高速に実行できます。
spring-boot-starter-test
スターターには、WebTestClient
の依存関係(spring-test
に含まれる)が含まれています。
テストクラスでは、Spring Bootのテスト機能と組み合わせてWebTestClient
のインスタンスを取得します。
“`java
package com.example.webfluxdemo.controller;
import com.example.webfluxdemo.model.User;
import com.example.webfluxdemo.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // ランダムなポートでWebFluxサーバーを起動
class UserControllerTest {
@Autowired
private WebTestClient webTestClient; // WebFluxアプリケーションにリクエストを送るクライアント
@Autowired
private UserRepository userRepository; // テストデータ準備のためにリポジトリを使用
@BeforeEach
void setUp() {
// 各テストの前にデータベースをクリアし、初期データを投入
// deleteAll().then() は、 deleteAllが完了した後に実行されるMono<Void>を返す
userRepository.deleteAll()
.then(userRepository.save(new User(null, "Alice", "[email protected]")))
.then(userRepository.save(new User(null, "Bob", "[email protected]")))
.block(); // テストセットアップなのでブロックしてもOK
}
@Test
void testGetAllUsers() {
webTestClient.get().uri("/api/users") // GETリクエストを送信
.accept(MediaType.APPLICATION_JSON) // Acceptヘッダーを設定
.exchange() // リクエストを実行
.expectStatus().isOk() // ステータスコードが200 OKであることを検証
.expectHeader().contentType(MediaType.APPLICATION_JSON) // Content-Typeヘッダーを検証
.expectBodyList(User.class) // レスポンスボディをUserのリストとして検証
.hasSize(2) // リストのサイズが2であることを検証
.contains(new User(null, "Alice", "[email protected]"), new User(null, "Bob", "[email protected]")); // リストに含まれる要素を検証 (IDは比較対象外にするためnullでインスタンス化)
}
@Test
void testGetUserById() {
webTestClient.get().uri("/api/users/{id}", 1L) // ID=1のユーザーを取得
.exchange()
.expectStatus().isOk()
.expectBody(User.class) // レスポンスボディをUserオブジェクトとして検証
.consumeWith(response -> { // レスポンスの内容に対して詳細な検証
User user = response.getResponseBody();
assert user != null;
assert user.getId() != null; // IDが設定されていることを検証
assert "Alice".equals(user.getName());
assert "[email protected]".equals(user.getEmail());
});
}
@Test
void testGetUserByIdNotFound() {
webTestClient.get().uri("/api/users/{id}", 99L) // 存在しないID
.exchange()
.expectStatus().isNotFound(); // ステータスコードが404 Not Foundであることを検証
}
@Test
void testCreateUser() {
User newUser = new User("Charlie", "[email protected]");
webTestClient.post().uri("/api/users")
.contentType(MediaType.APPLICATION_JSON) // Content-Typeヘッダーを設定
.body(Mono.just(newUser), User.class) // リクエストボディにMono<User>を設定
.exchange()
.expectStatus().isCreated() // ステータスコードが201 Createdであることを検証
.expectBody(User.class)
.consumeWith(response -> {
User createdUser = response.getResponseBody();
assert createdUser != null;
assert createdUser.getId() != null;
assert "Charlie".equals(createdUser.getName());
assert "[email protected]".equals(createdUser.getEmail());
});
// データベースに保存されているか確認(オプション)
userRepository.findByName("Charlie").block(); // ブロックして結果を取得
}
// testUpdateUser, testDeleteUser なども同様に記述
}
“`
@SpringBootTest(webEnvironment = SpringTest.WebEnvironment.RANDOM_PORT)
アノテーションは、ランダムなポートでWebFluxサーバーを起動し、そのURLをWebTestClient
が自動的に検出するようにします。
WebTestClient
の使い方は、uri()
でパスを指定し、method()
やショートカットメソッド(get()
, post()
など)でHTTPメソッドを指定、header()
やcontentType()
などでヘッダーを設定、body()
でリクエストボディを設定、そしてexchange()
でリクエストを実行します。
exchange()
の後には、様々な検証メソッドが続きます。expectStatus()
でステータスコード、expectHeader()
でヘッダー、expectBody()
またはexpectBodyList()
でレスポンスボディの内容を検証します。特に、expectBody(Class)
やexpectBodyList(Class)
は、レスポンスボディをリアクティブ型(Mono, Flux)として取得し、それを指定されたJava型に変換して検証できるようにします。
consumeWith()
メソッドを使うと、レスポンス全体に対してより柔軟な検証を行うことができます。
Functional Endpointsをテストする場合も、同じWebTestClient
を使用できます。@SpringBootTest
を使用せずに、テスト対象のRouterFunction
を直接WebTestClient
にバインドしてテストすることも可能です。
“`java
// Functional Endpointのテスト例 (RouterFunctionを直接バインド)
@ExtendWith(SpringExtension.class) // JUnit 5 + Spring
class UserRouterTest {
private WebTestClient webTestClient; // WebTestClientのインスタンス
// テスト対象のリポジトリ(モックなど)
private UserRepository userRepository = mock(UserRepository.class);
@BeforeEach
void setUp() {
// RouterFunctionを定義 (テスト対象)
UserHandler userHandler = new UserHandler(userRepository);
RouterFunction<?> userRoute = new UserRouter().routeUser(userHandler);
// WebTestClientにRouterFunctionをバインド
this.webTestClient = WebTestClient
.bindToRouterFunction(userRoute) // RouterFunctionをバインド
.configureClient()
.baseUrl("/api/functional/users") // ベースURLを設定
.build();
}
@Test
void testGetAllUsersFunctional() {
// モックのリポジトリの振る舞いを設定
when(userRepository.findAll()).thenReturn(
Flux.just(new User(1L, "Alice", "[email protected]"), new User(2L, "Bob", "[email protected]")));
webTestClient.get().uri("/") // ベースURLに対してURIを指定
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(User.class)
.hasSize(2);
// リポジトリのメソッドが呼び出されたことを検証
verify(userRepository).findAll();
}
// 他のテストメソッド...
}
``
RouterFunction
この Functional Endpoint のテスト例では、Spring コンテキスト全体を起動せず、テスト対象のに直接
WebTestClient` をバインドしています。これにより、より軽量で高速な単体テストが可能になります。依存するコンポーネント(例: リポジトリ)はモック化して利用します。
12. WebFluxの適切なユースケースと考慮事項
WebFluxは強力なフレームワークですが、全てのアプリケーションに適しているわけではありません。その特性を理解し、適切なユースケースで採用することが重要です。
WebFluxが特に適しているユースケース:
- 高スループットのI/Oバウンドなマイクロサービス: データベースアクセス、外部API呼び出し、メッセージキューとの通信など、I/O待ち時間がボトルネックになる処理が多いアプリケーション。API Gatewayやデータオーケストレーション層など。
- 多数の同時接続が想定されるアプリケーション: IoTバックエンド、モバイルアプリケーションのバックエンドなど、多数のクライアントからの接続を少ないリソースで効率的に捌く必要がある場合。
- リアルタイムアプリケーション: WebSocketやServer-Sent Events (SSE) を利用して、サーバーからクライアントにリアルタイムにデータをプッシュする必要があるアプリケーション。
- ノンブロッキングなエコシステムとの連携: Reactive Streamsをサポートするデータベース(R2DBC)、メッセージキュークライアント(Reactor Kafka, Reactor RabbitMQ)、HTTPクライアント(WebClient)などとシームレスに連携したい場合。
WebFluxを導入する際に考慮すべき事項:
- チームのリアクティブプログラミング習熟度: リアクティブな思考様式やデバッグ手法の習得には時間がかかります。チームメンバー全員がリアクティブプログラミングに慣れる必要があります。
- アプリケーションのボトルネック: アプリケーションのボトルネックが主にCPU演算である場合、WebFluxのメリットは限定的です。スレッドをブロックせずにCPUを使い切るような処理には、従来の命令型プログラミングの方がシンプルで効率的な場合があります。
- 既存ライブラリとの互換性: 使用したい既存のライブラリ(例: 認証ライブラリ、特定のデータソースドライバ)がリアクティブに対応しているか確認が必要です。対応していない場合は、アダプター層を開発するなどの対策が必要になり、複雑さが増します。
- プロジェクトの規模と複雑さ: 小規模でシンプルなアプリケーションであれば、WebFluxの導入は過剰な設計になる可能性があります。Spring MVCのシンプルさが適している場合も多いです。
- エラーハンドリングとデバッグの複雑さ: 非同期ストリームのエラーハンドリングやデバッグは、同期コードに比べて難易度が高くなります。
既存のSpring MVCアプリケーションをWebFluxに移行する場合、段階的に行うことも可能です。例えば、新しいI/OバウンドなサービスだけをWebFluxで開発し、既存のMVCサービスと連携させるといったアプローチが考えられます。Spring Bootでは、同じアプリケーション内でWebFluxとMVCを混在させることはできませんが、マイクロサービスアーキテクチャであれば異なるサービスで異なるスタックを選択できます。
13. まとめ
Spring WebFluxは、Spring Framework 5で導入された、ノンブロッキングI/Oとリアクティブプログラミングに基づく新しいWebフレームワークです。従来のSpring MVCがスレッド/リクエストモデルとブロッキングI/Oに基づいているのに対し、WebFluxはイベントループとノンブロッキングI/Oを採用しており、特にI/Oバウンドな処理や多数の同時接続において高いスケーラビリティと効率的なリソース利用を実現します。
WebFluxの中心的な要素は、ReactorライブラリのMono
とFlux
といったリアクティブ型、そしてReactive Streams仕様に基づくデータフロー処理です。プログラミングモデルとしては、Spring MVCライクなアノテーションベースのスタイルと、より関数型なFunctional Endpointsの2つが提供されています。データ永続化にはR2DBCのようなリアクティブなドライバ、外部API呼び出しにはWebClientといったノンブロッキングクライアントを使用します。
WebFluxは、高スループットのI/Oバウンドなマイクロサービスやリアルタイムアプリケーションなど、特定のユースケースで非常に強力な選択肢となります。しかし、リアクティブプログラミングの学習コスト、デバッグの難しさ、既存のブロッキングライブラリとの連携といったデメリットも存在します。
WebFluxの導入を検討する際は、アプリケーションの特性、パフォーマンス要件、チームのスキルセット、そして既存システムとの連携などを総合的に判断することが重要です。全ての場合においてWebFluxが最適解となるわけではありませんが、適切なシナリオで活用できれば、現代のWebアプリケーションに求められる高い応答性とスケーラビリティを実現するための強力な武器となるでしょう。
リアクティブプログラミングの習得は、WebFluxだけでなく、JVMエコシステムにおける非同期・並列処理を深く理解する上で非常に有益です。Spring WebFluxの学習を通じて、新しい時代のアプリケーション開発パラダイムに触れてみてください。