Spring WebFluxは、Spring Frameworkに含まれるノンブロッキングなWebフレームワークです。2017年9月のSpring Framework 5.0で加わり、2026年9月時点の最新はSpring Framework 7.0.9(Spring Boot 4.1.1が採用する版)です。少数のイベントループスレッドで多数のリクエストをさばくため、スレッドを止めない前提で書く必要があります。
この記事では、Spring MVCとの違い、ReactorのMonoとFlux、Spring Boot 4.1.1での最小構成、ブロッキング処理を混ぜたときの挙動を実測値つきで解説します。WebFluxを選ぶべきか迷っている方は、先にまとめと「採用すべき場面と避けるべき場面」の章を読んでください。
まとめ:Spring WebFluxの要点とMVCとの選び分け
- Spring WebFluxはReactive Streamsに基づくノンブロッキングなWebフレームワークで、既定のサーバーはNetty(Reactor Netty)です。
- Spring MVCは「リクエストごとにスレッドを占有してよい」前提、WebFluxは「スレッドを止めない」前提です。違いはこの前提と、それを受けたスレッドモデルにあります。
- 戻り値は1件なら
Mono(0〜1件)、複数件やストリームならFlux(0〜N件)を使います。 - JPAやJDBCのようなブロッキングAPIが中心なら、公式リファレンスもSpring MVCを推奨しています。WebFluxからリレーショナルDBへノンブロッキングでアクセスするなら、対応ドライバーのあるR2DBCが候補です。MongoDBなどには別のリアクティブ対応があります。
- Spring Boot 4ではWebClientの自動構成が別スターター(
spring-boot-starter-webclient)に分かれました。spring-boot-starter-webfluxだけではWebClient.BuilderのBeanが作られません。 - 動いているMVCアプリを移行する必要はありません。まず外部API呼び出しだけWebClientに置き換え、効果を測ってから広げます。Java 21以降では仮想スレッドという選択肢もあります。
Spring WebFluxの定義と2026年9月時点の構成バージョン
WebFluxは、Spring Frameworkのspring-webfluxモジュールです。Servlet APIに依存するSpring MVC(spring-webmvc)とは別系統の「リアクティブスタック」として、Spring Framework 5.0(GitHubのタグv5.0.0.RELEASEは2017年9月28日)で追加されました。リアクティブライブラリにはProject Reactorを使い、Reactive Streams仕様のバックプレッシャー(受け手が処理できる量だけ要求する仕組み)に対応します。
Spring Boot 4.1.1(2026年8月20日公開)でspring-boot-starter-webfluxを使った場合、BOM(依存バージョンをまとめて管理する定義)が引き込む主要ライブラリの版は次のとおりです。最新の対応表はSpring BootリファレンスのManaged Dependency Coordinatesで確認できます。
| ライブラリ | バージョン | 役割 |
|---|---|---|
| Spring Framework(spring-webflux) | 7.0.9 | WebFlux本体 |
| Reactor Core | 3.8.7 | Mono・Flux |
| Reactor Netty | 1.3.7 | HTTPサーバー/クライアント |
| Netty | 4.2.17.Final | 非同期I/O基盤 |
| Jackson | 3.1.5 | JSON変換 |
Spring Boot 4.1系のOSSサポートは2027年7月31日までです。4.0系は2026年12月31日で終わり、3.5系は2026年6月30日に終了しました。版ごとのサポート期限はSpring Bootのバージョン一覧とサポート期限で、4系の変更点全体はSpring Boot 4の変更点と3との違いで整理しています。
Spring MVCとの違い:スレッドモデルとブロッキングの前提
公式リファレンスの「Concurrency Model」は、両者の違いを「ブロッキングとスレッドに関する既定の前提」だと説明しています。Spring MVCはアプリケーションが現在のスレッドをブロックしうると考え、Servletコンテナは大きなスレッドプールでそのブロックを吸収します。WebFluxはアプリケーションがブロックしないと考え、少数の固定スレッド(イベントループ)でリクエストを処理します。
Reactor Nettyのイベントループ数は、既定で「JVMが使えるプロセッサ数(最小4)」です。16論理CPUのマシンでSpring Boot 4.1.1を起動すると、リクエストを処理するスレッドはreactor-http-nio-1からreactor-http-nio-16の16本でした。Tomcatの既定最大200スレッドとは桁が違います。
| 比較項目 | Spring MVC | Spring WebFlux |
|---|---|---|
| 基盤API | Servlet API | Reactive Streams |
| 既定サーバー | Tomcat | Netty(Reactor Netty) |
| 他の対応サーバー | Jetty | Tomcat・Jetty |
| スレッドの持ち方 | 1リクエスト1スレッド | 少数のイベントループ |
| ブロッキング呼び出し | 許容 | イベントループ上では不可 |
| コントローラーの戻り値 | 通常の型・Mono・Fluxなど | Mono・Flux・通常の型など |
| 書き方 | アノテーション/関数型 | アノテーション/関数型 |
| DBアクセス(RDB) | JDBC・JPA | R2DBC |
| Boot 4のスターター | spring-boot-starter-webmvc | spring-boot-starter-webflux |
@RestControllerや@GetMappingといったアノテーションは両方で使えるので、コードの見た目は似ています。一方、同じ書き方で中にブロッキング処理を入れると、WebFluxでは次の章で示すとおり性能が大きく落ちます。Spring MVC側の構成とSpring Boot 4での依存名の変更はSpring MVCの解説記事にまとめています。
両方のスターターを依存に入れた場合、Spring BootはSpring MVCを自動構成します。MVCアプリにWebClientだけを使う目的でWebFluxを追加する開発者が多いため、とSpring Bootのリファレンスは理由を説明しています。WebFluxで起動させたい場合はSpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)で明示します。
MonoとFluxの基本とReactive Streams
Mono(0〜1件)とFlux(0〜N件)の使い分け
Reactorのリファレンスは、Flux<T>を「0〜N個の要素を流す非同期シーケンス」、Mono<T>を「0〜1個の結果」と定義しています。どちらもReactive StreamsのPublisherで、正常終了時はonComplete、異常終了時はonErrorを通知します。ただし、終了しないストリームや、終了シグナルを通知せず購読がキャンセルされる場合もあります。
- IDで1件取得する、登録結果を返す:
Mono<User> - 一覧を返す、Server-Sent Eventsで流し続ける:
Flux<User> - 本文を返さない(削除など):
Mono<Void>
MonoやFluxを返すだけでは、処理が購読時まで遅延されるとは限りません。cold publisherは購読(subscribe)を契機に処理を始めますが、hot publisherは購読前から動く場合があります。Mono.just(load())ではload()がその場で実行され、Mono.fromCallable(() -> load())なら購読時まで遅延されます。WebFluxでは戻り値のPublisherをフレームワークが購読します。コントローラー内でblock()を呼んで値を取り出すと、イベントループを止める原因になります。
Reactorの主要オペレーターと処理の使い分け
MonoやFluxは、オペレーターをつないで処理を宣言します。最初に覚えるのは次の6つで足ります。
| オペレーター | 用途 |
|---|---|
| map | 値を同期的に変換 |
| flatMap | 値から別のMono/Fluxを呼び出して平坦化 |
| filter | 条件に合う要素だけ残す |
| zip | 複数の結果を待ち合わせて結合 |
| onErrorResume | エラー時に代替値へ切り替え |
| timeout | 応答の上限時間を設定 |
mapとflatMapの取り違えが最初のつまずきです。変換先が普通の値ならmap、WebClientやR2DBCの呼び出しのようにMonoを返す処理ならflatMapを使います。mapでMonoを返すと、型がMono<Mono<T>>になり、内側は誰にも購読されません。
Reactive Streamsとバックプレッシャーの位置づけ
Reactive Streamsは、Publisher・Subscriber・Subscription・Processorの4インターフェースで非同期ストリームの約束事を定めた仕様です。受け手がrequest(n)で処理できる件数を伝え、送り手はそれを超えて送りません。WebFluxはこの仕組みをHTTPの読み書きまで通しているので、遅いクライアントに大量データを流してもメモリにため込みにくくなります。流量制御の方式の比較はバックプレッシャーの仕組みと実装方式の選び分けで扱っています。
Spring Boot 4.1で最小のWebFluxアプリを作る手順
依存関係とspring-boot-starter-webfluxの中身
Spring Initializrで「Spring Reactive Web」を選び、Mavenプロジェクトを生成します。以下は依存関係部分の抜粋です。生成された@SpringBootApplication付きの起動クラスは残し、各サンプルを同じパッケージに配置してください。Spring Boot 4.1.1のspring-boot-starter-webfluxは、Jackson、Reactor Netty、spring-webfluxをまとめて引き込みます。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
テスト用のspring-boot-starter-webflux-testはSpring Boot 4で追加されたスターターで、WebTestClientとreactor-test(StepVerifier)を含みます。Spring Boot 3までのspring-boot-starter-testだけの構成から移行する場合は、これを足します。
アノテーション方式のコントローラー
Spring MVCと同じ@RestControllerで書けます。この例では戻り値にMono・Fluxを使います。Spring MVCもこれらの型を扱えるため、戻り値だけでは両者を区別できません。
package com.example.demo;
import java.time.Duration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class GreetingController {
@GetMapping("/hello/{name}")
public Mono<String> hello(@PathVariable String name) {
return Mono.just("Hello, " + name + " (" + Thread.currentThread().getName() + ")");
}
@GetMapping(value = "/ticks", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Long> ticks() {
return Flux.interval(Duration.ofSeconds(1)).take(3);
}
}
関数型エンドポイント(RouterFunction)の書き方
WebFluxには、ルーティングとハンドラーをラムダで書く関数型エンドポイントもあります。公式リファレンスは、小規模なアプリケーションや要件の単純なマイクロサービスで透明性と制御を重視する場合の選択肢に挙げています。アノテーション方式と同じアプリ内に共存できます。
package com.example.demo;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration
public class GreetingRouter {
@Bean
RouterFunction<ServerResponse> greetingRoutes() {
return route()
.GET("/fn/hello", request -> ServerResponse.ok()
.bodyValue("Hello from functional endpoint"))
.build();
}
}
起動ログとcurlでの動作確認
プロジェクト直下で./mvnw spring-boot:runを実行します。Java 25.0.4.1で起動すると、ログにNettyWebServer : Netty started on port 8080 (http)と出ます。Tomcatではなく、Nettyが立ち上がったことをここで確認できます。
$ curl localhost:8080/hello/issoh
Hello, issoh (reactor-http-nio-2)
$ curl localhost:8080/fn/hello
Hello from functional endpoint
$ curl -N localhost:8080/ticks
data:0
data:1
data:2
1つ目の応答に出ているreactor-http-nio-2がイベントループのスレッド名です。/ticksはServer-Sent Eventsとして1秒ごとに1件ずつ届きます。
ブロッキング処理の扱い:イベントループを止めない実装と実測
WebFluxで性能問題の原因になりやすいのは、イベントループ上でのブロッキング呼び出しです。JDBC、JPA、Thread.sleep、同期HTTPクライアント、ファイルI/Oなどが該当します。16本しかないスレッドのうち1本が止まると、そのスレッドに割り当てられた他のリクエストも待たされます。
1秒かかるブロッキング処理を、イベントループ上で直接実行するエンドポイントと、ReactorのboundedElasticスケジューラーへ逃がすエンドポイントを用意し、64リクエストを同時に送って比較しました。
@GetMapping("/bad")
public Mono<String> bad() throws InterruptedException {
Thread.sleep(1000);
return Mono.just(Thread.currentThread().getName());
}
@GetMapping("/good")
public Mono<String> good() {
return Mono.fromCallable(() -> {
Thread.sleep(1000);
return Thread.currentThread().getName();
})
.subscribeOn(Schedulers.boundedElastic());
}
| 実装 | 64件の所要時間 | 処理したスレッド |
|---|---|---|
| イベントループ上で直接ブロック | 4.60〜5.30秒 | reactor-http-nio 16本 |
| boundedElasticへ逃がす | 1.30〜1.31秒 | boundedElastic 64本 |
| Stringを返す+仮想スレッド有効 | 1.34秒 | 仮想スレッド 64本 |
計測環境は16論理CPUのMac(x86_64)、Spring Boot 4.1.1、Java 25.0.4.1です。負荷はPythonのurllibと64スレッドのThreadPoolExecutorで64件を同時に送り、全件の応答までの時間を測りました。1行目と2行目は2回ずつ計測した範囲、3行目は1回の値です。直接ブロックした場合、16本のイベントループが1秒ずつ占有されるので、64件は最低でも4巡(約4秒)かかります。boundedElasticへ逃がすと、64件がほぼ同時に1秒で終わりました。
3行目は、戻り値をMonoにせずStringを返すコントローラーで、Thread.sleep(1000)を呼んだ場合です。Spring Framework 6.1以降のWebFluxは、リアクティブ型以外を返すコントローラーメソッドを「ブロッキング」とみなし、AsyncTaskExecutorが設定されていればそちらで実行します。Spring Bootでspring.threads.virtual.enabled=trueにすると、このメソッドは仮想スレッドで動きました。設定しない場合は同じメソッドがreactor-http-nio上で動き、64件に4.75秒かかりました。
ただし、boundedElasticは既定でCPUコア数の10倍までしかスレッドを作らない有限のプールです。ブロッキング処理が大半を占めるアプリなら、それはSpring MVCのスレッドプールを手作りしているのと変わりません。逃がすのは一部の遅い呼び出しに限り、DBアクセスそのものをノンブロッキングにしたい場合はR2DBC(spring-boot-starter-data-r2dbc)を使います。JPAにはリアクティブ版がないため、「spring boot reactive jpa」という組み合わせはそのままでは成立しません。
WebClientによる外部API呼び出しとSpring Boot 4の依存変更
WebClientは、WebFluxと同じspring-webfluxモジュールに入っているノンブロッキングなHTTPクライアントです。公式リファレンスは、Spring MVCアプリケーションからでもWebClientを使えるとし、リアクティブ化の最初の一歩として勧めています。
Spring Boot 4で注意が必要なのが依存関係です。Spring Boot 4.1.1でspring-boot-starter-webfluxだけを入れた状態では、WebClient.BuilderのBeanが0件で、注入するとアプリが起動しません。WebClientの自動構成はspring-boot-starter-webclient(中身はspring-boot-webclientとreactor-netty-http)に分かれており、これを追加するとBeanが1件作られます。Spring Boot 3のサンプルをそのまま写すと、ここでつまずきます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webclient</artifactId>
</dependency>
package com.example.demo;
import java.time.Duration;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@Component
public class GreetingClient {
private final WebClient webClient;
public GreetingClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("http://localhost:8080").build();
}
public Mono<String> fetch(String name) {
return webClient.get()
.uri("/hello/{name}", name)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(2))
.onErrorResume(java.util.concurrent.TimeoutException.class,
e -> Mono.just("fallback"));
}
}
ブロッキングなSpring MVCアプリで同期的にHTTPを呼ぶだけなら、WebClientよりRestClientが素直です。使い分けとRestTemplateからの移行はSpringのRestClientの使い方で解説しています。
WebFluxを採用すべき場面と避けるべき場面
公式リファレンスの「Applicability」は、動作しているSpring MVCアプリケーションを変える必要はないと明言しています。さらに、幅広いアプリケーションで移行は不要だろうと述べています。WebFluxの採用は「新しいから」ではなく、次の条件で判断します。
WebFluxが向いている条件
- 外部APIやマイクロサービスへの呼び出しが多く、1回の待ち時間が長い、または呼び出し同士の依存が深い(公式は、この2つが大きいほど効果も大きいとしています)。
- Server-Sent EventsやWebSocketで、多数のクライアントへ長時間ストリームを流す。
- Spring Cloud Gatewayのように、I/O待ちが処理の大半を占めるゲートウェイやプロキシを作る。
- DBがR2DBC対応(PostgreSQL、MySQL、SQL Serverなど)で、データアクセスまでノンブロッキングで通せる。
WebFluxを避けるべき条件
- JPA・JDBC・同期SDKなどブロッキングなライブラリが中心。公式も、この場合は少なくとも一般的な構成ではSpring MVCが最適としています。
- チームにリアクティブの経験者がいない大規模開発。公式は、ノンブロッキング・関数型・宣言型への移行は学習曲線が急だと注意しています。スタックトレースが追いにくく、障害調査の難度も上がります。
- CPU負荷の高い計算が中心で、I/O待ちが少ない。WebFluxに変えても計算量は減らず、重い計算をイベントループ上で実行すると他のリクエスト処理を遅らせます。
Spring MVCの仮想スレッド利用とWebFluxとの選択基準
Java 21以降なら、Spring MVCのままspring.threads.virtual.enabled=trueを設定して仮想スレッドを使う方法もあります(Spring Bootリファレンスの「Virtual threads」)。ブロッキングな書き方のまま、待ち時間中にOSスレッドを手放せるので、「I/O待ちでスレッドが足りない」という課題の多くはこれで解決します。Spring Bootのリファレンスは、Java 24以降を強く推奨しています。また、仮想スレッドがキャリアスレッドに固定される「ピン留め」でスループットが落ちる場合があると注意しています。
前章の実測のとおり、WebFluxのアプリでも仮想スレッドを有効にすれば、Stringなどを返すブロッキングなコントローラーだけを仮想スレッドへ回せます。既存のMVCアプリの同時接続数を伸ばしたいなら仮想スレッド、ストリーミングやバックプレッシャー、非同期呼び出しの合成そのものが要件ならWebFlux、という線引きが実用的です。仮想スレッドの仕組みとピン留めの落とし穴はJava Virtual Thread(仮想スレッド)の解説を参照してください。
WebTestClientとStepVerifierによるテスト
WebFluxのコントローラーは@WebFluxTestとWebTestClientで、サーバーを起動せずにテストできます。Spring Boot 4では、@WebFluxTestのパッケージがorg.springframework.boot.webflux.test.autoconfigureに変わりました。
package com.example.demo;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webflux.test.autoconfigure.WebFluxTest;
import org.springframework.test.web.reactive.server.WebTestClient;
@WebFluxTest(GreetingController.class)
class GreetingControllerTest {
@Autowired
WebTestClient client;
@Test
void hello() {
client.get().uri("/hello/issoh").exchange()
.expectStatus().isOk()
.expectBody(String.class)
.value(body -> assertThat(body).startsWith("Hello, issoh"));
}
}
MonoやFluxそのものを検証するときは、reactor-testのStepVerifierを使います。流れてくる要素と完了シグナルを順番どおりに検証できます。
@Test
void mapAndFilter() {
Flux<String> flux = Flux.range(1, 5)
.filter(n -> n % 2 == 1)
.map(n -> "item-" + n);
StepVerifier.create(flux)
.expectNext("item-1", "item-3", "item-5")
.verifyComplete();
}
この記事のサンプルを含む検証用プロジェクトは、Spring Boot 4.1.1とJava 25.0.4.1の環境でmvn testを実行し、テスト5件がすべて成功することを確認済みです。
よくある質問
Spring WebFluxとは何ですか?
Spring Frameworkに含まれるノンブロッキングなWebフレームワークです。Spring Framework 5.0で追加され、Reactive StreamsとProject Reactorを基盤に、少数のイベントループスレッドで多数のリクエストを処理します。
Spring WebFluxとSpring MVCの違いは何ですか?
前提となるスレッドモデルが違います。Spring MVCはリクエストごとにスレッドを占有しブロッキングを許容し、WebFluxはスレッドを止めない前提でイベントループを使います。アノテーションは共通で、Mono・Fluxも両方で使えます。WebFluxでは通常の値を返すこともできます。
Spring WebFluxの既定のサーバーは何ですか?
Netty(Reactor Netty)です。Spring Boot 4.1.1のspring-boot-starter-webfluxはReactor Netty 1.3.7とNetty 4.2.17.Finalを引き込みます。TomcatやJettyに切り替えることもできます。
Spring WebFluxでJPAは使えますか?
技術的にはboundedElasticなどへ逃がせば呼べますが、ノンブロッキングの利点は失われます。JPAが中心なら公式もSpring MVCを推奨しており、リレーショナルDBへのアクセスまでノンブロッキングにするなら、対応ドライバーのあるR2DBC(Spring Data R2DBC)が候補です。
MonoとFluxはどう使い分けますか?
結果が0〜1件ならMono、0〜N件やストリームならFluxを使います。IDで1件取得する処理はMono、一覧取得やServer-Sent EventsはFluxが典型です。