异步请求
Spring MVC 与 Servlet 异步请求处理有广泛的集成:
有关这与 Spring WebFlux 有何不同的概述,请参见下面的异步 Spring MVC 与 WebFlux 的比较部分。
DeferredResult
一旦在 Servlet 容器中启用异步请求处理功能,控制器方法可以使用 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
运行给定任务来获取返回值。
处理
以下是 Servlet 异步请求处理的简要概述:
-
可以通过调用
request.startAsync()
将ServletRequest
置于异步模式。这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。 -
调用
request.startAsync()
会返回AsyncContext
,您可以使用它来进一步控制异步处理。例如,它提供了dispatch
方法,该方法类似于 Servlet API 中的转发,但它允许应用程序在 Servlet 容器线程上恢复请求处理。 -
ServletRequest
提供对当前DispatcherType
的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度类型。
DeferredResult
处理的工作原理如下:
-
控制器返回一个
DeferredResult
并将其保存在某个内存队列或列表中以便访问。 -
Spring MVC 调用
request.startAsync()
。 -
与此同时,
DispatcherServlet
和所有配置的过滤器退出请求处理线程,但响应保持打开状态。 -
应用程序从某个线程设置
DeferredResult
,然后 Spring MVC 将请求重新分派回 Servlet 容器。 -
再次调用
DispatcherServlet
,并使用异步生成的返回值继续处理。
Callable
处理过程如下:
-
控制器返回一个
Callable
。 -
Spring MVC 调用
request.startAsync()
并将Callable
提交给AsyncTaskExecutor
以在单独的线程中进行处理。 -
与此同时,
DispatcherServlet
和所有过滤器退出 Servlet 容器线程,但响应保持打开状态。 -
最终,
Callable
产生一个结果,Spring MVC 将请求调度回 Servlet 容器以完成处理。 -
再次调用
DispatcherServlet
,处理继续进行,并使用Callable
异步产生的返回值。
有关更多背景和上下文,您还可以阅读博客文章,其中介绍了 Spring MVC 3.2 中的异步请求处理支持。
异常处理
当你使用 DeferredResult
时,你可以选择调用 setResult
或者使用异常调用 setErrorResult
。在这两种情况下,Spring MVC 会将请求重新分派回 Servlet 容器以完成处理。然后,它会被视为控制器方法返回了给定的值,或者产生了给定的异常。异常随后会通过常规的异常处理机制(例如,调用 @ExceptionHandler
方法)进行处理。
当你使用 Callable
时,类似的处理逻辑会发生,主要的区别在于结果是从 Callable
返回的,或者由它抛出一个异常。
拦截
HandlerInterceptor
实例可以是 AsyncHandlerInterceptor
类型,以便在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted
回调(而不是 postHandle
和 afterCompletion
)。
HandlerInterceptor
实现还可以注册一个 CallableProcessingInterceptor
或 DeferredResultProcessingInterceptor
,以更深入地与异步请求的生命周期集成(例如,处理超时事件)。有关详细信息,请参见 AsyncHandlerInterceptor。
DeferredResult
提供了 onTimeout(Runnable)
和 onCompletion(Runnable)
回调。有关更多详细信息,请参阅 DeferredResult 的 javadoc。Callable
可以替换为 WebAsyncTask
,后者提供了用于超时和完成回调的附加方法。
异步 Spring MVC 与 WebFlux 的比较
Servlet API 最初是为通过 Filter-Servlet 链进行单次传递而构建的。异步请求处理允许应用程序退出 Filter-Servlet 链,但保留响应以供进一步处理。Spring MVC 的异步支持是围绕这一机制构建的。当控制器返回一个 DeferredResult
时,Filter-Servlet 链被退出,Servlet 容器线程被释放。稍后,当 DeferredResult
被设置时,会进行一次 ASYNC
调度(到相同的 URL),在此期间,控制器再次被映射,但不会调用它,而是使用 DeferredResult
值(就像控制器返回它一样)来恢复处理。
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的异步请求处理功能,因为它在设计上就是异步的。异步处理被内置于所有框架契约中,并在请求处理的所有阶段得到本质上的支持。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步和反应式类型作为控制器方法的返回值。Spring MVC 甚至支持流式处理,包括反应式背压。然而,响应的每次写入仍然是阻塞的(并在一个单独的线程上执行),这与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O,并且不需要为每次写入分配额外的线程。
另一个根本区别在于,Spring MVC 不支持在控制器方法参数中使用异步或响应式类型(例如,@RequestBody
、@RequestPart
等),也没有对作为模型属性的异步和响应式类型的任何显式支持。而 Spring WebFlux 支持所有这些功能。
最后,从配置的角度来看,异步请求处理功能必须在 Servlet 容器级别启用。
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
。相反,servlet 容器会自动启动一个 AsyncListener
错误通知,其中 Spring MVC 会进行 completeWithError
调用。此调用反过来会对应用程序执行最后一次 ASYNC
派发,在此期间 Spring MVC 调用配置的异常解析器并完成请求。
SSE
SseEmitter
(ResponseBodyEmitter
的子类)提供对服务器发送事件的支持,其中从服务器发送的事件根据 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 不支持 Server-Sent Events。可以考虑使用 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...
}
您可以在 ResponseEntity
中使用 StreamingResponseBody
作为主体,以自定义响应的状态和头信息。
Reactive 类型
Spring MVC 支持在控制器中使用响应式客户端库(另请参阅 WebFlux 部分中的响应式库)。这包括来自 spring-webflux
的 WebClient
和其他库,例如 Spring Data 响应式数据存储库。在这种情况下,能够从控制器方法返回响应式类型是很方便的。
响应式返回值的处理方式如下:
-
单值 promise 被适配,类似于使用
DeferredResult
。示例包括Mono
(Reactor)或Single
(RxJava)。 -
使用流媒体类型(例如
application/x-ndjson
或text/event-stream
)的多值流被适配,类似于使用ResponseBodyEmitter
或SseEmitter
。示例包括Flux
(Reactor)或Observable
(RxJava)。应用程序还可以返回Flux<ServerSentEvent>
或Observable<ServerSentEvent>
。 -
使用其他任何媒体类型(例如
application/json
)的多值流被适配,类似于使用DeferredResult<List<?>>
。
Spring MVC 通过 spring-core
中的 ReactiveAdapterRegistry 支持 Reactor 和 RxJava,这使得它可以适配多个响应式库。
对于响应的流式传输,支持响应式背压,但对响应的写入仍然是阻塞的,并且通过配置的 AsyncTaskExecutor
在单独的线程上运行,以避免阻塞上游源,例如从 WebClient
返回的 Flux
。
上下文传播
通过 java.lang.ThreadLocal
传播上下文是常见的做法。这在同一线程上处理时可以透明地工作,但在跨多个线程的异步处理时需要额外的工作。Micrometer Context Propagation 库简化了跨线程的上下文传播,以及跨上下文机制如 ThreadLocal
值、Reactor context、GraphQL Java context 等的传播。
如果 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()) {
// ...
}
以下 ThreadLocalAccessor
实现是开箱即用的:
-
LocaleContextThreadLocalAccessor
— 通过LocaleContextHolder
传播LocaleContext
-
RequestAttributesThreadLocalAccessor
— 通过RequestContextHolder
传播RequestAttributes
上述内容不会自动注册。您需要在启动时通过 ContextRegistry.getInstance()
注册它们。
有关更多详细信息,请参阅 Micrometer Context Propagation 库的文档。
断开连接
Servlet API 不提供任何通知来告知远程客户端断开连接。因此,在向响应流式传输数据时,无论是通过 SseEmitter 还是 reactive types,定期发送数据是很重要的,因为如果客户端已断开连接,写操作将失败。发送的数据可以是一个空的(仅注释)SSE 事件或任何其他数据,另一方需要将其解释为心跳并忽略。
或者,考虑使用具有内置心跳机制的 Web 消息传递解决方案(例如 STOMP over WebSocket 或带有 SockJS 的 WebSocket)。
配置
异步请求处理功能必须在 Servlet 容器级别启用。MVC 配置还提供了几个用于异步请求的选项。
Servlet 容器
Filter 和 Servlet 声明有一个 asyncSupported
标志,需要将其设置为 true
以启用异步请求处理。此外,Filter 映射应声明以处理 ASYNC
jakarta.servlet.DispatchType
。
在 Java 配置中,当你使用 AbstractAnnotationConfigDispatcherServletInitializer
来初始化 Servlet 容器时,这个过程是自动完成的。
在 web.xml
配置中,你可以在 DispatcherServlet
和 Filter
声明中添加 <async-supported>true</async-supported>
,并在过滤器映射中添加 <dispatcher>ASYNC</dispatcher>
。
Spring MVC
MVC 配置为异步请求处理提供了以下选项:
-
Java 配置:使用
WebMvcConfigurer
中的configureAsyncSupport
回调。 -
XML 命名空间:在
<mvc:annotation-driven>
下使用<async-support>
元素。
你可以配置以下内容:
-
异步请求的默认超时时间取决于底层的 Servlet 容器,除非明确设置。
-
AsyncTaskExecutor
用于在使用反应式类型进行流式传输时进行阻塞写入,以及用于执行从控制器方法返回的Callable
实例。默认使用的执行器在负载下不适合生产环境。 -
DeferredResultProcessingInterceptor
实现和CallableProcessingInterceptor
实现。
注意,您还可以在 DeferredResult
、ResponseBodyEmitter
和 SseEmitter
上设置默认超时时间值。对于 Callable
,您可以使用 WebAsyncTask
来提供超时时间值。