对于最新稳定版本,请使用 Spring Framework 7.0.6spring-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

控制器可以使用 java.util.concurrent.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

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

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

  • 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

  • DispatcherServlet 再次被调用,并使用异步生成的返回值继续处理。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

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

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

异常处理

当你使用 DeferredResult 时,可以选择调用 setResult 或者使用异常调用 setErrorResult。在这两种情况下,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,以便更深入地集成到异步请求的生命周期中(例如,处理超时事件)。请参阅 AsyncHandlerInterceptor 以获取更多详细信息。spring-doc.cadn.net.cn

DeferredResult 提供 onTimeout(Runnable)onCompletion(Runnable) 回调。 请参阅 DeferredResult 的 Javadoc 以获取更多详情。Callable 可替代 WebAsyncTask,后者暴露了用于超时和完成回调的额外方法。spring-doc.cadn.net.cn

异步 Spring MVC 与 WebFlux 对比

Servlet API 最初是为单次遍历 Filter-Servlet 链而设计的。异步请求处理允许应用程序退出 Filter-Servlet 链,同时保持响应处于打开状态以进行后续处理。Spring MVC 的异步支持正是围绕这一机制构建的。当控制器返回一个 DeferredResult 时,Filter-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)

SseEmitterResponseBodyEmitter 的子类)提供了对服务器发送事件(Server-Sent Events)的支持,其中从服务器发送的事件按照 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()

虽然 SSE(服务器发送事件)是向浏览器进行流式传输的主要选项,但请注意 Internet Explorer 并不支持 Server-Sent Events。可以考虑使用 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

  • 单值 Promise 被适配为类似于使用 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

  • 一个多值流(multi-value stream)会适配为其他任意媒体类型(例如 application/json),其方式类似于使用 DeferredResult<List<?>>spring-doc.cadn.net.cn

Spring MVC 通过来自spring-coreReactiveAdapterRegistry支持 Reactor 和 RxJava,这使其能够适配多种响应式库。

对于向响应进行流式传输,支持响应式背压(reactive back pressure),但对响应的写入仍然是阻塞的,并通过配置的 TaskExecutor 在单独的线程中执行,以避免阻塞上游数据源(例如从 Flux 返回的 WebClient)。 默认情况下,阻塞写入使用 SimpleAsyncTaskExecutor,但在高负载下并不适用。如果您计划使用响应式类型进行流式传输,则应使用MVC 配置来配置一个任务执行器(task executor)。spring-doc.cadn.net.cn

上下文传播

通过 java.lang.ThreadLocal 传播上下文是很常见的做法。这种方式在线程内部透明地工作,但在跨多个线程进行异步处理时则需要额外的工作。Micrometer 的 上下文传播(Context Propagation) 库简化了跨线程以及跨不同上下文机制(例如 ThreadLocal 值、 Reactor 上下文、 GraphQL Java 上下文 等)的上下文传播。spring-doc.cadn.net.cn

如果类路径中存在 Micrometer 上下文传播(Context Propagation)库,当控制器方法返回一个响应式类型(例如 FluxMono)时,所有已注册了 ThreadLocalio.micrometer.ThreadLocalAccessor 值都会以键值对的形式写入 Reactor 的 Context 中,其中键由 ThreadLocalAccessor 指定。spring-doc.cadn.net.cn

对于其他异步处理场景,您可以直接使用上下文传播(Context Propagation)库。例如: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 Context Propagation 库的文档。spring-doc.cadn.net.cn

断开连接

Servlet API 在远程客户端断开连接时不会发出任何通知。 因此,在向响应流式传输数据时(无论是通过 SseEmitter 还是 响应式类型),定期发送数据非常重要, 因为如果客户端已断开连接,写入操作将会失败。这种发送可以采用一个空的(仅包含注释的)SSE 事件形式, 或者采用其他任何对方需将其解释为心跳信号并忽略的数据形式。spring-doc.cadn.net.cn

或者,考虑使用内置心跳机制的 Web 消息解决方案(例如 基于 WebSocket 的 STOMP 或结合 SockJS 的 WebSocket)。spring-doc.cadn.net.cn

配置

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

Servlet 容器

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

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

web.xml 配置中,您可以为 <async-supported>true</async-supported>DispatcherServlet 声明添加 Filter,并为过滤器映射添加 <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

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

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

请注意,您也可以在 DeferredResultResponseBodyEmitterSseEmitter 上设置默认超时值。对于 Callable,您可以使用 WebAsyncTask 来指定超时值。spring-doc.cadn.net.cn