1. 概述

本文我们将学习如何在Spring MVC中使用 DeferredResult 来执行异步请求处理。

异步支持是在Servlet 3.0中引入的,它允许在接收请求的线程中新开另一个线程处理HTTP请求,同时客户端连接不会被中断。

Spring 3.2 开始提供DeferredResult,允许将耗时任务从HTTP Worker线程放到独立线程中执行,Work线程可以继续新的HTTP请求而不会被阻塞,大大提供系统吞吐量,特别是对于IO密集型应用。

2. 阻塞请求

我们平常开发的Controller默认就是阻塞,请求处理完成并返回结果之前,Http Worker线程会一直阻塞。

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

3. 使用DeferredResult实现非阻塞

为了避免Worker线程阻塞,我们使用回调编程模型,返回类型改为DeferredResult,真实的结果通过回调的方式通知:

@GetMapping("/async-deferredresult")
public DeferredResult<ResponseEntity<?>> handleReqDefResult(Model model) {
    LOG.info("收到 async-deferredresult 请求");
    DeferredResult<ResponseEntity<?>> output = new DeferredResult<>();
    
    ForkJoinPool.commonPool().submit(() -> {
        LOG.info("在新线程中执行");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
        }
        output.setResult(ResponseEntity.ok("ok"));
    });
    
    LOG.info("servlet 线程释放");
    return output;
}

耗时任务在新线程中处理,这里我们用sleep模拟耗时操作。执行完成后调用DeferredResult的setResult方法通知结果

我们可以通过查看日志验证结果:

[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
收到 async-deferredresult 请求
[nio-8080-exec-6] com.baeldung.controller.AsyncDeferredResultController: 
Servlet 线程释放
[nio-8080-exec-6] java.lang.Thread : 在新线程中执行

4. 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.")));

下面模拟超时操作:

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."));
});

5. 总结

本文,我们研究了SpringMVC DeferredResult 如何简化异步接口的创建。

文中代码可在 Github 中查看。