1. 概述
在本教程中,我們將探討如何使用 Spring MVC 中的 DeferredResult 類來執行異步請求處理。
異步支持在 Servlet 3.0 中引入,簡單來説,它允許在與請求接收器線程不同的線程中處理 HTTP 請求。
DeferredResult,從 Spring 3.2 版本開始可用,有助於將長時間運行的計算從 http-worker 線程卸載到單獨的線程。
雖然計算線程會佔用一些資源,但 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 到 Servlet 容器。
@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;
}請求處理在單獨的線程中執行,完成後我們調用 setResult 操作於 DeferredResult 對象上。
讓我們查看日誌輸出以確認我們的線程行為是否符合預期:
[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 響應被交付給客户端。連接將由容器(Servlet 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 如何簡化異步端點的創建。