非同期リクエスト

  • コントローラーメソッドの DeferredResult および Callable の戻り値は、単一の非同期戻り値の基本的なサポートを提供します。

  • コントローラーは、SSE生データを含む複数の値をストリーミングできます。

  • コントローラーは、リアクティブクライアントを使用して、レスポンス処理にリアクティブ型を返すことができます。

これが Spring WebFlux とどのように異なるかの概要については、以下の非同期 Spring MVC と WebFlux の比較セクションを参照してください。

DeferredResult

サーブレットコンテナーで非同期リクエスト処理機能を有効にすると、次の例に示すように、コントローラーメソッドは DeferredResult でサポートされているコントローラーメソッドの戻り値をラップできます。

  • Java

  • Kotlin

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
	DeferredResult<String> deferredResult = new DeferredResult<>();
	// Save the deferredResult somewhere..
	return deferredResult;
}

// From some other thread...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
	val deferredResult = DeferredResult<String>()
	// Save the deferredResult somewhere..
	return deferredResult
}

// From some other thread...
deferredResult.setResult(result)

コントローラーは、外部イベント(JMS メッセージ)、スケジュールされたタスク、その他のイベントへのレスポンスなど、異なるスレッドから非同期的に戻り値を生成できます。

Callable

次の例に示すように、コントローラーは、サポートされている戻り値を java.util.concurrent.Callable でラップできます。

  • Java

  • Kotlin

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
	return () -> "someView";
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
	// ...
	"someView"
}

その後、構成された  AsyncTaskExecutor を介して特定のタスクを実行することにより、戻り値を取得できます。

処理

サーブレットの非同期リクエスト処理の非常に簡潔な概要を次に示します。

  • request.startAsync() を呼び出すことにより、ServletRequest を非同期モードにすることができます。これを行うことの主な効果は、サーブレット(およびすべてのフィルター)を終了できることですが、レスポンスは開いたままで、後で処理を完了できます。

  • request.startAsync() の呼び出しは AsyncContext を返し、これを使用して非同期処理をさらに制御できます。例: dispatch メソッドを提供します。これは、サーブレット API からの転送に似ていますが、アプリケーションがサーブレットコンテナースレッドでのリクエスト処理を再開できる点が異なります。

  • ServletRequest は、現在の DispatcherType へのアクセスを提供します。これを使用して、初期リクエスト、非同期ディスパッチ、転送、その他のディスパッチャー型の処理を区別できます。

DeferredResult 処理は次のように機能します。

  • コントローラーは DeferredResult を返し、アクセス可能なメモリ内キューまたはリストに保存します。

  • Spring MVC は request.startAsync() を呼び出します。

  • 一方、DispatcherServlet およびすべての構成済みフィルターはリクエスト処理スレッドを終了しますが、レスポンスは開いたままです。

  • アプリケーションは何らかのスレッドから DeferredResult を設定し、Spring MVC はリクエストをサーブレットコンテナーにディスパッチします。

  • DispatcherServlet が再度呼び出され、非同期に生成された戻り値で処理が再開されます。

Callable 処理は次のように機能します。

  • コントローラーは Callable を返します。

  • Spring MVC は request.startAsync() を呼び出し、別のスレッドで処理するために Callable を AsyncTaskExecutor に送信します。

  • 一方、DispatcherServlet およびすべてのフィルターはサーブレットコンテナースレッドを終了しますが、レスポンスは開いたままです。

  • 最終的に Callable は結果を生成し、Spring MVC はリクエストをサーブレットコンテナーにディスパッチして処理を完了します。

  • DispatcherServlet が再び呼び出され、Callable から非同期的に生成された戻り値で処理が再開されます。

詳細な背景とコンテキストについては、Spring MVC 3.2 での非同期リクエスト処理サポートを導入したブログ投稿を読 (英語) むこともできます。

例外処理

DeferredResult を使用する場合、例外付きで setResult または setErrorResult を呼び出すかどうかを選択できます。どちらの場合も、Spring MVC はリクエストをサーブレットコンテナーにディスパッチして処理を完了します。その後、コントローラーメソッドが指定された値を返したか、指定された例外を生成したかのように処理されます。その後、例外は通常の例外処理メカニズム (たとえば、@ExceptionHandler メソッドの呼び出し) を通過します。

Callable を使用すると、同様の処理ロジックが発生しますが、主な違いは、結果が Callable から返されるか、例外が発生することです。

インターセプト

HandlerInterceptor インスタンスは型 AsyncHandlerInterceptor で、非同期処理を開始する最初のリクエストで afterConcurrentHandlingStarted コールバックを受け取ることができます(postHandle および afterCompletion の代わりに)。

HandlerInterceptor 実装では、CallableProcessingInterceptor または DeferredResultProcessingInterceptor を登録して、非同期リクエストのライフサイクルとより深く統合することもできます(たとえば、タイムアウトイベントを処理するため)。詳細については、AsyncHandlerInterceptor (Javadoc) を参照してください。

DeferredResult は、onTimeout(Runnable) および onCompletion(Runnable) コールバックを提供します。詳細については、DeferredResult の javadoc を参照してください。Callable は、タイムアウトおよび完了コールバックの追加メソッドを公開する WebAsyncTask の代わりに使用できます。

非同期 Spring MVC と WebFlux の比較

サーブレット API は元々、Filter-Servlet チェーンをシングルパスするために構築されました。非同期リクエスト処理により、アプリケーションは Filter-Servlet チェーンを終了できますが、さらなる処理のためにレスポンスを開いたままにします。Spring MVC 非同期サポートは、そのメカニズムを中心に構築されています。コントローラーが DeferredResult を返すと、Filter-Servlet チェーンが終了し、サーブレットコンテナースレッドが解放されます。後で、DeferredResult が設定されると、(同じ URL への) ASYNC ディスパッチが行われ、その間にコントローラーが再度マップされますが、DeferredResult 値は、それを呼び出すのではなく、(コントローラーがそれを返したかのように)使用して処理を再開します。

対照的に、Spring WebFlux はサーブレット API 上に構築されておらず、設計上非同期であるため、そのような非同期リクエスト処理機能も必要ありません。非同期処理はすべてのフレームワーク契約に組み込まれており、リクエスト処理のすべての段階で本質的にサポートされています。

プログラミングモデルの観点から見ると、Spring MVC と Spring WebFlux はどちらも、コントローラーメソッドの戻り値として非同期とリアクティブ型をサポートしています。Spring MVC は、リアクティブバックプレッシャを含むストリーミングもサポートします。ただし、ノンブロッキング I/O に依存し、書き込みごとに追加のスレッドを必要としない WebFlux とは異なり、レスポンスへの個々の書き込みはブロックされたままになります (別のスレッドで実行されます)。

もう 1 つの基本的な違いは、Spring MVC はコントローラーメソッドの引数で非同期型またはリアクティブ型 (たとえば、@RequestBody@RequestPart など) をサポートしておらず、モデル属性として非同期型およびリアクティブ型を明示的にサポートしていないことです。Spring WebFlux はそのすべてをサポートします。

最後に、構成の観点から、非同期リクエスト処理機能をサーブレットコンテナーレベルで有効にする必要があります。

HTTP ストリーミング

単一の非同期戻り値に DeferredResult および Callable を使用できます。複数の非同期値を生成し、レスポンスに書き込む場合はどうなるでしょうか? このセクションでは、その方法について説明します。

オブジェクト

ResponseBodyEmitter 戻り値を使用してオブジェクトのストリームを生成できます。次の例に示すように、各オブジェクトは HttpMessageConverter で直列化され、レスポンスに書き込まれます。

  • Java

  • Kotlin

@GetMapping("/events")
public ResponseBodyEmitter handle() {
	ResponseBodyEmitter emitter = new ResponseBodyEmitter();
	// Save the emitter somewhere..
	return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
	// Save the emitter somewhere..
}

// In some other thread
emitter.send("Hello once")

// and again later on
emitter.send("Hello again")

// and done at some point
emitter.complete()

ResponseBodyEmitter を ResponseEntity の本文として使用して、レスポンスのステータスとヘッダーをカスタマイズすることもできます。

emitter が IOException をスローする場合 (たとえば、リモートクライアントがなくなった場合)、アプリケーションは接続をクリーンアップする責任がないため、emitter.complete または emitter.completeWithError を呼び出すべきではありません。代わりに、サーブレットコンテナーは AsyncListener エラー通知を自動的に開始し、Spring MVC が completeWithError 呼び出しを行います。次に、この呼び出しによって、アプリケーションへの最終的な ASYNC ディスパッチが 1 回実行されます。その間、Spring MVC は構成済みの例外リゾルバーを呼び出し、リクエストを完了します。

SSE

SseEmitter (ResponseBodyEmitter のサブクラス)は、サーバー送信イベント [W3C] (英語) のサポートを提供します。この場合、サーバーから送信されたイベントは、W3C SSE 仕様に従ってフォーマットされます。コントローラーから SSE ストリームを生成するには、次の例に示すように SseEmitter を返します。

  • Java

  • Kotlin

@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
	SseEmitter emitter = new SseEmitter();
	// Save the emitter somewhere..
	return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
	// Save the emitter somewhere..
}

// In some other thread
emitter.send("Hello once")

// and again later on
emitter.send("Hello again")

// and done at some point
emitter.complete()

SSE はブラウザーへのストリーミングの主なオプションですが、Internet Explorer はサーバー送信イベントをサポートしていないことに注意してください。Spring の WebSocket メッセージングを、幅広いブラウザーを対象とする SockJS フォールバックトランスポート(SSE を含む)とともに使用することを検討してください。

例外処理に関する注意については、前のセクションも参照してください。

生データ

メッセージの変換をバイパスして、レスポンス OutputStream に直接ストリーミングすると便利な場合があります(ファイルのダウンロードなど)。次の例に示すように、StreamingResponseBody 戻り値型を使用してこれを行うことができます。

  • Java

  • Kotlin

@GetMapping("/download")
public StreamingResponseBody handle() {
	return new StreamingResponseBody() {
		@Override
		public void writeTo(OutputStream outputStream) throws IOException {
			// write...
		}
	};
}
@GetMapping("/download")
fun handle() = StreamingResponseBody {
	// write...
}

StreamingResponseBody を ResponseEntity の本文として使用して、レスポンスのステータスとヘッダーをカスタマイズできます。

リアクティブ型

Spring MVC は、コントローラーでのリアクティブクライアントライブラリの使用をサポートします (WebFlux セクションのリアクティブライブラリも参照してください)。これには、spring-webflux の WebClient や、Spring Data リアクティブデータリポジトリなどのその他のものが含まれます。このようなシナリオでは、コントローラーメソッドからリアクティブ型を返すことができると便利です。

リアクティブな戻り値は次のように処理されます。

  • DeferredResult を使用するのと同様に、単一値の promise が適応されます。例には、Mono (Reactor)または Single (RxJava)が含まれます。

  • ResponseBodyEmitter または SseEmitter を使用するのと同様に、ストリーミングメディア型(application/x-ndjson または text/event-stream など)を持つ多値ストリームが適応されます。例には、Flux (Reactor)または Observable (RxJava)が含まれます。アプリケーションは Flux<ServerSentEvent> または Observable<ServerSentEvent> を返すこともできます。

  • 他のメディア型(application/json など)を持つ複数値ストリームは、DeferredResult<List<?>> を使用するのと同様に適応されます。

Spring MVC は、spring-coreReactiveAdapterRegistry (Javadoc) を介して Reactor および RxJava をサポートします。これにより、複数のリアクティブライブラリから適応できます。

レスポンスへのストリーミングでは、リアクティブバックプレッシャーがサポートされていますが、レスポンスへの書き込みは依然としてブロックされており、WebClient から返される Flux などの上流ソースのブロックを避けるために、構成された AsyncTaskExecutor を通じて別のスレッドで実行されます。

コンテキストの伝播

java.lang.ThreadLocal を介してコンテキストを伝搬するのが一般的です。これは、同じスレッドでの処理に対して透過的に機能しますが、複数のスレッドにわたる非同期処理には追加の作業が必要です。Micrometer コンテキストの伝播 [GitHub] (英語) ライブラリは、スレッド間、ThreadLocal 値、Reactor コンテキスト (英語) 、GraphQL Java コンテキスト (英語) などのコンテキストメカニズム間のコンテキスト伝播を簡素化します。

Micrometer Context Propagation がクラスパスに存在する場合、コントローラーメソッドが Flux や Mono などのリアクティブ型を返すと、登録された io.micrometer.ThreadLocalAccessor があるすべての ThreadLocal 値がキーと値のペアとして Reactor Context に書き込まれます。ThreadLocalAccessor によって割り当てられたキー。

その他の非同期処理シナリオでは、Context Propagation ライブラリを直接使用できます。例:

Java
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();

// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
	// ...
}

次の ThreadLocalAccessor 実装がすぐに使用できます。

  • LocaleContextThreadLocalAccessor —  LocaleContextHolder を介して LocaleContext を伝播する

  • RequestAttributesThreadLocalAccessor —  RequestContextHolder を介して RequestAttributes を伝播する

上記は自動登録されません。起動時に ContextRegistry.getInstance() 経由で登録する必要があります。

詳細については、Micrometer Context Propagation ライブラリのドキュメント (英語) を参照してください。

切断

Servlet API は、リモートクライアントがなくなると通知を行いません。SseEmitter またはリアクティブ型のいずれを介しても、レスポンスにストリーミングしている間、クライアントが切断された場合は書き込みが失敗するため、定期的にデータを送信することが重要です。送信は、空の(コメントのみの)SSE イベント、または相手側がハートビートとして解釈して無視する必要があるその他のデータの形式を取ることができます。

または、組み込みのハートビートメカニズムを備えた Web メッセージングソリューション(WebSocket を介した STOMPSockJS 付き WebSocket など)の使用を検討してください。

構成

非同期リクエスト処理機能は、サーブレットコンテナーレベルで有効にする必要があります。MVC 構成は、非同期リクエストのいくつかのオプションも公開します。

サーブレットコンテナー

フィルター宣言とサーブレット宣言には asyncSupported フラグがあり、非同期リクエスト処理を有効にするには、true に設定する必要があります。さらに、フィルターマッピングは、ASYNC jakarta.servlet.DispatchType を処理するように宣言する必要があります。

Java 構成では、AbstractAnnotationConfigDispatcherServletInitializer を使用してサーブレットコンテナーを初期化すると、これは自動的に行われます。

web.xml 構成では、<async-supported>true</async-supported> を DispatcherServlet および Filter 宣言に追加し、<dispatcher>ASYNC</dispatcher> をフィルターマッピングに追加できます。

Spring MVC

MVC 構成では、非同期リクエスト処理用に次のオプションが公開されています。

  • Java 構成: WebMvcConfigurer で configureAsyncSupport コールバックを使用します。

  • XML 名前空間: <mvc:annotation-driven> の <async-support> 要素を使用します。

以下を構成できます。

  • 非同期リクエストのデフォルトのタイムアウト値は、明示的に設定されていない限り、基盤となるサーブレットコンテナーによって異なります。

  • AsyncTaskExecutor は、リアクティブ型でストリーミングするときに書き込みをブロックするため、およびコントローラーメソッドから返された Callable インスタンスを実行するために使用します。デフォルトで使用されるものは、負荷がかかる運用には適していません。

  • DeferredResultProcessingInterceptor 実装および CallableProcessingInterceptor 実装。

DeferredResultResponseBodyEmitterSseEmitter にデフォルトのタイムアウト値を設定することもできます。Callable の場合、WebAsyncTask を使用してタイムアウト値を提供できます。