异步请求
DeferredResult
一旦在Servlet容器中启用了异步请求处理功能,控制器方法可以将任何支持的控制器方法返回值与DeferredResult
包装起来,如下例所示:
-
Java
-
Kotlin
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<>();
// 将deferredResult保存在某处..
return deferredResult;
}
// 从其他线程...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// 将deferredResult保存在某处..
return deferredResult
}
// 从其他线程...
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。可以用WebAsyncTask
替换Callable
,后者公开了用于超时和完成回调的附加方法。
Spring MVC异步与WebFlux比较
Servlet API最初是为通过过滤器-Servlet链进行单次遍历而构建的。异步请求处理允许应用程序退出过滤器-Servlet链,但保持响应打开以进行进一步处理。Spring MVC的异步支持是围绕该机制构建的。当控制器返回一个DeferredResult
时,退出了过滤器-Servlet链,并释放了Servlet容器线程。稍后,当设置DeferredResult
时,将进行ASYNC
分派(到相同的URL),在此期间控制器再次映射,但不是调用它,而是使用DeferredResult
值(就像控制器返回它一样)来恢复处理。
相比之下,Spring WebFlux既不建立在Servlet API之上,也不需要这样的异步请求处理功能,因为它本身就是异步的设计。异步处理内置于所有框架合同中,并通过请求处理的所有阶段得到内在支持。
从编程模型的角度来看,Spring MVC和Spring WebFlux都支持控制器方法中作为返回值的异步和响应式类型。Spring MVC甚至支持流式处理,包括响应式背压。但是,对响应的单个写入仍然是阻塞的(并且在单独的线程上执行),与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();
// 将emitter保存在某处..
return emitter;
}
// 在其他线程中
emitter.send("Hello once");
// 稍后再次发送
emitter.send("Hello again");
// 最后完成
emitter.complete();
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
// 将emitter保存在某处..
}
// 在其他线程中
emitter.send("Hello once")
// 稍后再次发送
emitter.send("Hello again")
// 最后完成
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();
// 将emitter保存在某处..
return emitter;
}
// 在其他线程中
emitter.send("Hello once");
// 稍后再次发送
emitter.send("Hello again");
// 最后完成
emitter.complete();
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
// 将emitter保存在某处..
}
// 在其他线程中
emitter.send("Hello once")
// 稍后再次发送
emitter.send("Hello again")
// 最后完成
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 {
// 写入...
}
};
}
@GetMapping("/download")
fun handle() = StreamingResponseBody {
// 写入...
}
您可以将StreamingResponseBody
用作ResponseEntity
中的主体,以自定义响应的状态和标头。
响应式类型
Spring MVC支持在控制器中使用响应式客户端库(还请阅读WebFlux部分中的响应式库)。这包括来自spring-webflux
等的WebClient
以及其他一些,如Spring Data响应式数据存储库。在这种情况下,从控制器方法返回响应式类型非常方便。
响应式返回值处理如下:
-
单值承诺会被适配,类似于使用
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
)。
Context Propagation
It is common to propagate context via java.lang.ThreadLocal
. This works transparently for handling on the same thread, but requires additional work for asynchronous handling across multiple threads. The Micrometer Context Propagation library simplifies context propagation across threads, and across context mechanisms such as ThreadLocal
values, Reactor context, GraphQL Java context, and others.
If Micrometer Context Propagation is present on the classpath, when a controller method returns a reactive type such as Flux
or Mono
, all ThreadLocal
values, for which there is a registered io.micrometer.ThreadLocalAccessor
, are written to the Reactor Context
as key-value pairs, using the key assigned by the ThreadLocalAccessor
.
For other asynchronous handling scenarios, you can use the Context Propagation library directly. For example:
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();
// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
// ...
}
For more details, see the documentation of the Micrometer Context Propagation library.
Disconnects
The Servlet API does not provide any notification when a remote client goes away. Therefore, while streaming to the response, whether through SseEmitter or reactive types, it is important to send data periodically, since the write fails if the client has disconnected. The send could take the form of an empty (comment-only) SSE event or any other data that the other side would have to interpret as a heartbeat and ignore.
Alternatively, consider using web messaging solutions (such as STOMP over WebSocket or WebSocket with SockJS) that have a built-in heartbeat mechanism.
Configuration
The asynchronous request processing feature must be enabled at the Servlet container level. The MVC configuration also exposes several options for asynchronous requests.
Servlet Container
Filter and Servlet declarations have an asyncSupported
flag that needs to be set to true
to enable asynchronous request processing. In addition, Filter mappings should be declared to handle the ASYNC
jakarta.servlet.DispatchType
.
In Java configuration, when you use AbstractAnnotationConfigDispatcherServletInitializer
to initialize the Servlet container, this is done automatically.
In web.xml
configuration, you can add <async-supported>true</async-supported>
to the DispatcherServlet
and to Filter
declarations and add <dispatcher>ASYNC</dispatcher>
to filter mappings.
Spring MVC
The MVC configuration exposes the following options for asynchronous request processing:
-
Java configuration: Use the
configureAsyncSupport
callback onWebMvcConfigurer
. -
XML namespace: Use the
<async-support>
element under<mvc:annotation-driven>
.
You can configure the following:
-
Default timeout value for async requests, which if not set, depends on the underlying Servlet container.
-
AsyncTaskExecutor
to use for blocking writes when streaming with Reactive Types and for executingCallable
instances returned from controller methods. The one used by default is not suitable for production under load. -
DeferredResultProcessingInterceptor
implementations andCallableProcessingInterceptor
implementations.
Note that you can also set the default timeout value on a DeferredResult
, a ResponseBodyEmitter
, and an SseEmitter
. For a Callable
, you can use WebAsyncTask
to provide a timeout value.