1. 概要

このチュートリアルでは、SpringMVCのDeferredResultクラスを使用して非同期リクエスト処理を実行する方法を見ていきます。

非同期サポートはサーブレット3.0で導入され、簡単に言えば、リクエストレシーバースレッドとは別のスレッドでHTTPリクエストを処理できるようになりました。

DeferredResult、 Spring 3.2以降で利用可能で、長時間実行される計算をhttp-workerスレッドから別のスレッドにオフロードするのに役立ちます。

他のスレッドは計算にいくらかのリソースを使用しますが、ワーカースレッドはその間ブロックされず、着信クライアント要求を処理できます。

非同期要求処理モデルは、特にIOを集中的に使用する操作の場合に、高負荷時にアプリケーションを適切にスケーリングするのに役立つため、非常に便利です。

2. 設定

この例では、Spring Bootアプリケーションを使用します。 アプリケーションをブートストラップする方法の詳細については、以前の記事を参照してください。

次に、 DeferredResult を使用した同期通信と非同期通信の両方を示し、高負荷でIOを集中的に使用するユースケースで非同期通信がどのように拡張されるかを比較します。

3. RESTサービスのブロック

標準のブロッキングRESTサービスの開発から始めましょう:

@GetMapping("/process-blocking")
public ResponseEntity<?> handleReqSync(Model model) { 
    // ...
    return ResponseEntity.ok("ok");
}

ここでの問題は、完全なリクエストが処理され、結果が返されるまで、リクエスト処理スレッドがブロックされることです。 長時間実行される計算の場合、これは最適ではないソリューションです。

これに対処するために、次のセクションで説明するように、コンテナースレッドをより有効に活用してクライアント要求を処理できます。

4. DeferredResultを使用したノンブロッキングREST

ブロッキングを回避するために、実際の結果の代わりにDeferredResultをサーブレットコンテナに返すコールバックベースのプログラミングモデルを使用します。

@GetMapping("/async-deferredresult")
public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) {
    LOG.info("Received async-deferredresult request");
    DeferredResult<ResponseEntity<?>> output = new DeferredResult<>();
    
    ForkJoinPool.commonPool().submit(() -> {
        LOG.info("Processing in separate thread");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
        }
        output.setResult(ResponseEntity.ok("ok"));
    });
    
    LOG.info("servlet thread freed");
    return output;
}

リクエスト処理は別のスレッドで実行され、完了すると、DeferredResultオブジェクトに対してsetResult操作が呼び出されます。

ログ出力を見て、スレッドが期待どおりに動作することを確認しましょう。

[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Received async-deferredresult request
[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Servlet thread freed
[nio-8080-exec-6] java.lang.Thread : Processing in separate thread

内部的には、コンテナスレッドに通知され、HTTP応答がクライアントに配信されます。 応答が到着するかタイムアウトするまで、接続はコンテナ(サーブレット3.0以降)によって開いたままになります。

5. DeferredResultコールバック

DeferredResultには、完了、タイムアウト、エラーの3種類のコールバックを登録できます。

onCompletion()メソッドを使用して、非同期リクエストが完了したときに実行されるコードのブロックを定義しましょう。

deferredResult.onCompletion(() -> LOG.info("Processing complete"));

同様に、 onTimeout()を使用して、タイムアウトが発生したときに呼び出すカスタムコードを登録できます。 リクエストの処理時間を制限するために、DeferredResultオブジェクトの作成中にタイムアウト値を渡すことができます。

DeferredResult<ResponseEntity<?>> deferredResult = new DeferredResult<>(500l);

deferredResult.onTimeout(() -> 
  deferredResult.setErrorResult(
    ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
      .body("Request timeout occurred.")));

タイムアウトの場合、DeferredResultに登録されているタイムアウトハンドラーを介して異なる応答ステータスを設定しています。

定義されたタイムアウト値である5秒を超えるリクエストを処理して、タイムアウトエラーをトリガーしましょう。

ForkJoinPool.commonPool().submit(() -> {
    LOG.info("Processing in separate thread");
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        ...
    }
    deferredResult.setResult(ResponseEntity.ok("OK")));
});

ログを見てみましょう:

[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
servlet thread freed
[nio-8080-exec-6] java.lang.Thread: Processing in separate thread
[nio-8080-exec-6] com.baeldung.controller.DeferredResultController: 
Request timeout occurred

何らかのエラーまたは例外が原因で、長時間実行される計算が失敗するシナリオがあります。 この場合、 onError()コールバックを登録することもできます。

deferredResult.onError((Throwable t) -> {
    deferredResult.setErrorResult(
      ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body("An error occurred."));
});

エラーが発生した場合、応答の計算中に、このエラーハンドラーを介して異なる応答ステータスとメッセージ本文を設定します。

6. 結論

この簡単な記事では、Spring MVC DeferredResultが非同期エンドポイントの作成をどのように容易にするかを見てきました。

いつものように、完全なソースコードはGithubから入手できます。