对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

异步请求

Spring MVC与Servlet异步请求处理具有深度集成:spring-doc.cadn.net.cn

要了解此方法与Spring WebFlux的区别,请参阅下面的异步Spring MVC与WebFlux的对比章节。spring-doc.cadn.net.cn

DeferredResult

一旦在Servlet容器中启用了异步请求处理功能,控制器方法可以将任何受支持的控制器方法返回值包装在DeferredResult中,如下例所示:spring-doc.cadn.net.cn

@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消息)、定时任务或其他事件。spring-doc.cadn.net.cn

Callable

一个控制器可以包装任何受支持的返回值,如下例所示:spring-doc.cadn.net.cn

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
	return () -> "someView";
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
	// ...
	"someView"
}

然后可以通过将给定的任务通过配置的 TaskExecutor来获得返回值。spring-doc.cadn.net.cn

处理

这里是一个关于Servlet异步请求处理的非常简洁的概述:spring-doc.cadn.net.cn

  • 一个 ServletRequest 可以通过调用 request.startAsync() 切换为异步模式。 这样做的主要效果是Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。spring-doc.cadn.net.cn

  • 调用 request.startAsync() 返回 AsyncContext,你可以将其用于进一步控制异步处理。例如,它提供了 dispatch 方法,该方法类似于 Servlet API 中的转发,但允许应用程序在 Servlet 容器线程上恢复请求处理。spring-doc.cadn.net.cn

  • The ServletRequest 提供对当前 DispatcherType 的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度器类型。spring-doc.cadn.net.cn

DeferredResult 处理如下:spring-doc.cadn.net.cn

  • 控制器返回一个 DeferredResult 并将其保存在某个内存队列或列表中,以便可以访问。spring-doc.cadn.net.cn

  • Spring MVC 调用 request.startAsync()spring-doc.cadn.net.cn

  • 与此同时,DispatcherServlet 和所有配置的过滤器退出请求处理线程,但响应仍然保持打开状态。spring-doc.cadn.net.cn

  • 应用程序在某个线程中设置 DeferredResult,然后Spring MVC将请求分发回Servlet容器。spring-doc.cadn.net.cn

  • The DispatcherServlet is invoked again, and processing resumes with the asynchronously produced return value.spring-doc.cadn.net.cn

Callable 处理如下:spring-doc.cadn.net.cn

  • 控制器返回一个 Callablespring-doc.cadn.net.cn

  • Spring MVC 调用 request.startAsync() 并将 Callable 提交到 一个 TaskExecutor 以在单独的线程中进行处理。spring-doc.cadn.net.cn

  • 与此同时,DispatcherServlet 和所有过滤器退出Servlet容器线程, 但响应仍然保持打开状态。spring-doc.cadn.net.cn

  • 最终 Callable 生成了结果,Spring MVC 将请求分发回 Servlet 容器以完成处理。spring-doc.cadn.net.cn

  • The DispatcherServlet 被再次调用,处理从 Callable 异步生成的返回值继续进行。spring-doc.cadn.net.cn

有关进一步的背景和上下文,你还可以阅读 介绍异步请求处理支持的博客文章,这些文章介绍了Spring MVC 3.2中的异步请求处理支持。spring-doc.cadn.net.cn

异常处理

当你使用 DeferredResult 时,你可以选择是否调用 setResultsetErrorResult 并抛出异常。在这两种情况下,Spring MVC 将请求重新分发回 Servlet 容器以完成处理。然后,它将被视为控制器方法返回给定值或产生给定异常。然后该异常将通过常规异常处理机制(例如,调用 @ExceptionHandler 方法)。spring-doc.cadn.net.cn

当你使用 Callable 时,类似的处理逻辑会发生,主要区别在于结果是从 Callable 返回的,或者它会抛出异常。spring-doc.cadn.net.cn

拦截

HandlerInterceptor 个实例可以是 AsyncHandlerInterceptor 类型,以在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted 回调(而不是 postHandleafterCompletion)。spring-doc.cadn.net.cn

HandlerInterceptor 实现也可以注册一个 CallableProcessingInterceptor 或一个 DeferredResultProcessingInterceptor, 以更深入地集成到异步请求的生命周期中(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptorspring-doc.cadn.net.cn

DeferredResult 提供了 onTimeout(Runnable)onCompletion(Runnable) 回调。 请参阅 DeferredResult 的 javadoc》 以获取更多详细信息。Callable 可以替换为 WebAsyncTask,后者提供了超时和完成回调的附加方法。spring-doc.cadn.net.cn

异步Spring MVC与WebFlux的对比

Servlet API最初设计用于单次通过过滤器-Servlet链的处理。异步请求处理允许应用程序退出过滤器-Servlet链,同时保持响应开启以进行后续处理。Spring MVC的异步支持正是基于此机制构建。当控制器返回DeferredResult时,系统将退出过滤器-Servlet链并释放Servlet容器线程。随后,当DeferredResult被设置时,系统会发起一次ASYNC调度(指向相同URL),在此过程中控制器会再次被映射,但不会调用控制器本身,而是使用DeferredResult值(如同控制器返回该值)来恢复处理流程。spring-doc.cadn.net.cn

相比之下,Spring WebFlux 并不是基于 Servlet API 构建的,也不需要这样的异步请求处理功能,因为它在设计上就是异步的。异步处理内置在所有框架合约中,并在整个请求处理过程中得到内在支持。spring-doc.cadn.net.cn

从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持在控制器方法中将异步和响应式类型作为返回值。Spring MVC 甚至支持流式传输,包括响应式背压。然而,与 WebFlux 不同的是,响应中的单个写操作仍然是阻塞的(并且是在单独的线程上执行的),而 WebFlux 依赖于非阻塞 I/O,并且不需要为每个写操作额外分配一个线程。spring-doc.cadn.net.cn

另一个基本的区别是,Spring MVC 不支持在控制器方法参数中使用异步或响应式类型(例如,@RequestBody@RequestPart 等),也不支持将异步和响应式类型作为模型属性。Spring WebFlux 支持所有这些功能。spring-doc.cadn.net.cn

最后,从配置角度看,必须在 Servlet容器级别启用异步请求处理功能。spring-doc.cadn.net.cn

HTTP流

您可以使用 DeferredResultCallable 来获取单个异步返回值。 如果您想生成多个异步值并将它们写入响应,该怎么办?本节描述了如何实现这一点。spring-doc.cadn.net.cn

对象

您可以使用 ResponseBodyEmitter 返回值来生成一个对象流,其中每个对象都使用 HttpMessageConverter 序列化并写入响应,如下例所示:spring-doc.cadn.net.cn

@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 中的正文,让您 自定义响应的状态和标头。spring-doc.cadn.net.cn

当一个emitter抛出一个IOException(例如,如果远程客户端断开连接),应用程序不需要负责清理连接,也不应该调用emitter.completeemitter.completeWithError。相反,servlet容器会自动触发一个AsyncListener错误通知,在这个过程中Spring MVC会进行一次completeWithError调用。这次调用反过来会对应用程序进行最后一次ASYNC调度,在此期间Spring MVC会调用配置的异常解析器并完成请求。spring-doc.cadn.net.cn

SSE

SseEmitter (一个 ResponseBodyEmitter 的子类) 提供了对 服务器发送事件 的支持,其中从服务器发送的事件按照 W3C SSE 规范进行格式化。要从控制器生成一个 SSE 流,请返回 SseEmitter,如下例所示:spring-doc.cadn.net.cn

@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()

While SSE是浏览器流媒体的主要选项,请注意Internet Explorer不支持服务器发送事件。考虑使用Spring的WebSocket消息传递,并使用SockJS后备传输(包括SSE),以针对广泛的浏览器。spring-doc.cadn.net.cn

另见 前一节 有关异常处理的说明。spring-doc.cadn.net.cn

原始数据

有时,跳过消息转换并直接流式传输到响应可能是有用的 OutputStream(例如,用于文件下载)。您可以使用StreamingResponseBody 返回值类型来实现这一点,如下例所示:spring-doc.cadn.net.cn

@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-doc.cadn.net.cn

响应式类型

Spring MVC 支持在控制器中使用响应式客户端库(请参阅WebFlux部分的响应式库)。 这包括来自WebClientspring-webflux等,以及其他如Spring Data响应式数据存储库。在这些情况下,能够从控制器方法返回响应式类型非常方便。spring-doc.cadn.net.cn

响应式返回值的处理方式如下:spring-doc.cadn.net.cn

  • 单值承诺被适配,类似于使用 DeferredResult。示例包括 Mono (Reactor) 或 Single (RxJava)。spring-doc.cadn.net.cn

  • 一个多值流,具有流媒体类型(如application/x-ndjsontext/event-stream),会被适配,类似于使用ResponseBodyEmitterSseEmitter。示例包括Flux(Reactor)或Observable(RxJava)。应用程序还可以返回Flux<ServerSentEvent>Observable<ServerSentEvent>spring-doc.cadn.net.cn

  • 一个多值流,带有任何其他媒体类型(如application/json)会被适应,类似于使用DeferredResult<List<?>>spring-doc.cadn.net.cn

Spring MVC 通过来自 spring-coreReactiveAdapterRegistry 支持 Reactor 和 RxJava,这使得它可以适应多种响应式库。

对于流式传输到响应,支持响应式背压,但写入响应仍然阻塞,并且在通过配置的 TaskExecutor上运行,以避免阻塞上游源(例如从WebClient返回的Flux)。默认情况下,使用SimpleAsyncTaskExecutor进行阻塞写入,但这在负载下并不合适。如果您计划使用响应式类型进行流式传输,应使用MVC配置来配置任务执行器。spring-doc.cadn.net.cn

上下文传播

通过 java.lang.ThreadLocal 传播上下文是常见的。这在同一个线程上处理时工作透明,但需要在多个线程之间进行异步处理时额外工作。Micrometer 的 上下文传播 库简化了跨线程的上下文传播,以及跨上下文机制,如 ThreadLocal 值、 Reactor 上下文、 GraphQL Java 上下文、 等。spring-doc.cadn.net.cn

如果在类路径中存在 Micrometer 上下文传播,当控制器方法返回 响应式类型(例如 FluxMono)时,所有已注册 io.micrometer.ThreadLocalAccessorThreadLocal 值都将以键值对形式写入 Reactor Context 中,使用的键由 ThreadLocalAccessor 分配。spring-doc.cadn.net.cn

对于其他异步处理场景,您可以直接使用上下文传播库。例如:spring-doc.cadn.net.cn

Java
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();

// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
	// ...
}

更多详情请参阅 文档中的Micrometer上下文传播库说明。spring-doc.cadn.net.cn

断开连接

Servlet API 没有提供任何通知来指示远程客户端何时断开连接。因此,在通过 SseEmitter响应式类型 流式传输响应时,定期发送数据非常重要,因为如果客户端已断开连接,写入会失败。可以发送空的(仅注释)SSE 事件或任何其他数据,对方端需要将其解释为心跳并忽略它。spring-doc.cadn.net.cn

Alternatively, consider using web messaging solutions (such as STOMP over WebSocket or WebSocket with SockJS) that have a built-in heartbeat mechanism.spring-doc.cadn.net.cn

配置

异步请求处理功能必须在Servlet容器级别启用。 MVC配置还提供了几个用于异步请求的选项。spring-doc.cadn.net.cn

Servlet 容器

Filter 和 Servlet 声明有一个 asyncSupported 标志,需要设置为 true 以启用异步请求处理。此外,Filter 映射应声明为处理 ASYNC jakarta.servlet.DispatchTypespring-doc.cadn.net.cn

在Java配置中,当你使用AbstractAnnotationConfigDispatcherServletInitializer 来初始化Servlet容器时,这是自动完成的。spring-doc.cadn.net.cn

web.xml 配置中,您可以将 <async-supported>true</async-supported> 添加到 DispatcherServletFilter 声明中,并将 <dispatcher>ASYNC</dispatcher> 添加到过滤器映射中。spring-doc.cadn.net.cn

Spring MVC

MVC配置暴露了以下与异步请求处理相关的选项:spring-doc.cadn.net.cn

您可以配置以下内容:spring-doc.cadn.net.cn

  • 异步请求的默认超时值,如果未设置,则取决于底层的Servlet容器。spring-doc.cadn.net.cn

  • AsyncTaskExecutor 用于在使用 响应式类型进行流式传输时阻塞写入,以及执行从控制器方法返回的Callable实例。我们强烈建议配置此属性,如果您使用响应式类型进行流式传输或有返回Callable的控制器方法,默认情况下它是SimpleAsyncTaskExecutorspring-doc.cadn.net.cn

  • DeferredResultProcessingInterceptor 个实现和 CallableProcessingInterceptor 个实现。spring-doc.cadn.net.cn

请注意,您还可以为 DeferredResultResponseBodyEmitterSseEmitter 设置默认超时值。对于 Callable,您可以使用 WebAsyncTask 来提供超时值。spring-doc.cadn.net.cn