异步请求
Spring MVC 与 Servlet 异步请求 处理 进行了广泛集成
-
控制器方法中的
DeferredResult、Callable和WebAsyncTask返回值提供对单个异步返回值的支持。 -
控制器可以使用反应式客户端并返回 反应式类型 进行响应处理。
有关此与 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 获取返回值。
WebAsyncTask
WebAsyncTask 与使用 Callable 类似,但允许自定义其他设置,例如请求超时值和用于执行 java.util.concurrent.Callable 的 AsyncTaskExecutor,而不是全局为 Spring MVC 设置的默认值。以下是使用 WebAsyncTask 的示例
-
Java
-
Kotlin
@GetMapping("/callable")
WebAsyncTask<String> handle() {
return new WebAsyncTask<String>(20000L,()->{
Thread.sleep(10000); //simulate long-running task
return "asynchronous request completed";
});
}
@GetMapping("/callable")
fun handle(): WebAsyncTask<String> {
return WebAsyncTask(20000L) {
Thread.sleep(10000) // simulate long-running task
"asynchronous request completed"
}
}
处理
以下是 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 不支持服务器发送事件。考虑使用 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...
}
您可以将 StreamingResponseBody 用作 ResponseEntity 的主体,以自定义响应的状态和标头。
反应式类型
Spring MVC 支持在控制器中使用反应式客户端库(另请阅读 WebFlux 部分中的 反应式库)。这包括 spring-webflux 中的 WebClient 和其他,例如 Spring Data 反应式数据仓库。在这种情况下,能够从控制器方法返回反应式类型会很方便。
反应式返回值的处理方式如下
-
单值 Promise 被适配,类似于使用
DeferredResult。示例包括CompletionStage(JDK)、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 上下文传播 库简化了跨线程以及跨上下文机制(例如 ThreadLocal 值、Reactor 上下文、GraphQL Java 上下文 等)的上下文传播。
如果 Micrometer Context Propagation 存在于类路径中,当控制器方法返回 反应式类型(例如 Flux 或 Mono)时,所有已注册 io.micrometer.ThreadLocalAccessor 的 ThreadLocal 值将使用 ThreadLocalAccessor 分配的键写入 Reactor Context 作为键值对。
对于其他异步处理场景,您可以直接使用 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 还是 反应式类型,定期发送数据都很重要,因为如果客户端已断开连接,写入将失败。发送可以是空(仅注释)SSE 事件或任何其他数据,对方必须将其解释为心跳并忽略。
或者,考虑使用具有内置心跳机制的网络消息解决方案(例如 通过 WebSocket 的 STOMP 或带有 SockJS 的 WebSocket)。
配置
必须在 Servlet 容器级别启用异步请求处理功能。MVC 配置还公开了异步请求的几个选项。
Servlet 容器
过滤器和 Servlet 声明有一个 asyncSupported 标志,需要将其设置为 true 以启用异步请求处理。此外,过滤器映射应声明为处理 ASYNC jakarta.servlet.DispatchType。
在 Java 配置中,当您使用 AbstractAnnotationConfigDispatcherServletInitializer 初始化 Servlet 容器时,这会自动完成。
在 web.xml 配置中,您可以将 <async-supported>true</async-supported> 添加到 DispatcherServlet 和 Filter 声明中,并将 <dispatcher>ASYNC</dispatcher> 添加到过滤器映射中。
Spring MVC
MVC 配置公开了异步请求处理的以下选项
-
Java 配置:在
WebMvcConfigurer上使用configureAsyncSupport回调。 -
XML 命名空间:在
<mvc:annotation-driven>下使用<async-support>元素。
您可以配置以下内容
-
异步请求的默认超时值取决于底层 Servlet 容器,除非显式设置。
-
用于在使用 反应式类型 进行流式传输时进行阻塞写入以及执行从控制器方法返回的
Callable实例的AsyncTaskExecutor。默认使用的那个不适用于生产负载。 -
DeferredResultProcessingInterceptor实现和CallableProcessingInterceptor实现。
请注意,您还可以设置 DeferredResult、ResponseBodyEmitter 和 SseEmitter 的默认超时值。对于 Callable,您可以使用 WebAsyncTask 来提供超时值。