异步请求 (Asynchronous Requests)
Spring MVC 与 Servlet 3.0+ 的异步请求处理有着深入的集成。异步处理允许控制器释放 Servlet 线程,转而在后台线程处理耗时操作,从而提高应用系统的吞吐量。
核心组件
1. DeferredResult
DeferredResult 允许你在不同的线程中设置处理结果。你只需在控制器中返回它,Spring 会保持连接打开,直到你调用了 setResult。
java
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<>();
// 将 deferredResult 交给外部事件监听器或后台任务处理
return deferredResult;
}
// 在其他线程或任务完成后:
deferredResult.setResult(result);kotlin
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// ...
return deferredResult
}2. Callable
控制器可以返回 Callable。Spring MVC 会自动调用 AsyncTaskExecutor 来异步执行它,当任务完成后,再将结果返回给 Servlet 容器进行后续处理。
java
@PostMapping("/upload")
public Callable<String> processUpload(MultipartFile file) {
return () -> {
// 耗时的文件处理逻辑
return "viewName";
};
}3. WebAsyncTask
类似于 Callable,但提供了更多的配置选项,如超时处理(Timeout)和指定特定的执行器(Executor)。
HTTP 流式处理 (Streaming)
如果你需要产生多个异步值并将它们逐步写入响应,可以使用流式处理。
1. ResponseBodyEmitter
用于发送一系列对象,每个对象都会通过 HttpMessageConverter 序列化。
2. SseEmitter
ResponseBodyEmitter 的子类,专门用于 Server-Sent Events (SSE),数据格式符合 W3C SSE 规范。
3. StreamingResponseBody
用于直接绕过消息转换,直接向响应的 OutputStream 写入原始数据(非常适合大文件下载)。
响应式类型 (Reactive Types)
Spring MVC 甚至支持在控制器中返回响应式类型,如 Mono 或 Flux (Reactor),或者 Observable (RxJava)。
- 单值类型(如
Mono):效果类似于DeferredResult。 - 多值流(如
Flux):如果 Media Type 是text/event-stream或application/x-ndjson,效果类似于 SSE 流。
配置要求
- Servlet 容器:必须在
web.xml中将<async-supported>true</async-supported>添加到DispatcherServlet和所有 Filter,或者在 Java 配置中自动启用。 - 线程池:默认使用的线程池不适合在高负载生产环境下使用,务必配置自定义的
AsyncTaskExecutor。
补充教学
1. 异步请求的“往返”流程
当你返回 DeferredResult 或 Callable 时:
- Servlet 线程进入控制器,启动异步模式,然后立即返回线程池(线程变为空闲)。
- 此时请求并未结束,连接由 Servlet 容器保持,但没有任何线程在阻塞等待。
- 业务逻辑在后台线程计算完毕,调用
setResult。 - Spring MVC 会发起一次内部分发 (Internal Dispatch)。
- Servlet 线程再次被分配来处理结果,就像控制器刚返回了这个值一样进行视图渲染。
2. Spring MVC 异步 vs WebFlux
- Spring MVC:基于 Servlet 3.0+ 规范。它解决了“线程在等待 IO 时被阻塞”的问题,但在写入响应(Write)时仍然是阻塞的(尽管是在专门的线程池里写)。
- WebFlux:从头到尾的非阻塞。不仅路由和逻辑是非阻塞的,连底层网络 IO 写入也是非阻塞的。它不需要为每个并发连接分配这么重的线程维护,资源利用率更高。
3. 超时处理的最佳实践
异步请求最大的风险是客户端断开了连接而服务端却还在跑。
- 始终为
DeferredResult设置超时时间:new DeferredResult<>(timeoutValue, errorResult)。 - 捕获
AsyncRequestTimeoutException并通过@ExceptionHandler返回友好的提示,避免连接池假死。