非同期リクエスト
Spring MVC は、サーブレットの非同期リクエスト処理と広範囲に統合されています。
これが 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-core の ReactiveAdapterRegistry (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 ライブラリを直接使用できます。例:
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();
// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
// ...
}
詳細については、Micrometer Context Propagation ライブラリのドキュメント (英語) を参照してください。
切断
Servlet API は、リモートクライアントがなくなると通知を行いません。SseEmitter またはリアクティブ型のいずれを介しても、レスポンスにストリーミングしている間、クライアントが切断された場合は書き込みが失敗するため、定期的にデータを送信することが重要です。送信は、空の(コメントのみの)SSE イベント、または相手側がハートビートとして解釈して無視する必要があるその他のデータの形式を取ることができます。
または、組み込みのハートビートメカニズムを備えた Web メッセージングソリューション(WebSocket を介した STOMP や SockJS 付き 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
実装。
DeferredResult
、ResponseBodyEmitter
、SseEmitter
にデフォルトのタイムアウト値を設定することもできます。Callable
の場合、WebAsyncTask
を使用してタイムアウト値を提供できます。