异步请求

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 运行给定任务来获取返回值。

WebAsyncTask

WebAsyncTask 与使用 Callable 类似,但允许自定义附加设置,例如请求超时值,以及用于执行 java.util.concurrent.CallableAsyncTaskExecutor,而不是 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 时,您可以选择调用 setResultsetErrorResult 并带有一个异常。在这两种情况下,Spring MVC 都会将请求调度回 Servlet 容器以完成处理。然后,它被视为控制器方法返回了给定值,或者它产生了给定异常。 然后,异常会通过常规异常处理机制(例如,调用 @ExceptionHandler 方法)。

当您使用 Callable 时,会发生类似的处理逻辑,主要区别在于 结果是从 Callable 返回的,或者由它引发异常。

拦截

HandlerInterceptor 实例可以是 AsyncHandlerInterceptor 类型,以便在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted 回调(而不是 postHandleafterCompletion)。

HandlerInterceptor 实现还可以注册 CallableProcessingInterceptorDeferredResultProcessingInterceptor,以便更深入地集成异步请求的生命周期(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptor

DeferredResult 提供 onTimeout(Runnable)onCompletion(Runnable) 回调。 有关更多详细信息,请参见 DeferredResult 的 JavadocCallable 可以替换为 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 流式传输

您可以将 DeferredResultCallable 用于单个异步返回值。 如果您想生成多个异步值并将其写入响应,该怎么办?本节描述如何做到这一点。

对象

您可以使用 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.completeemitter.completeWithError。相反,servlet 容器会自动启动 AsyncListener 错误通知,其中 Spring MVC 会调用 completeWithError。 此调用反过来会向应用程序执行一次最终的 ASYNC 调度,在此期间 Spring MVC 调用配置的异常解析器并完成请求。

SSE

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

您可以将 StreamingResponseBody 用作 ResponseEntity 中的正文以 自定义响应的状态和标头。

响应式类型

Spring MVC 支持在控制器中使用响应式客户端库(另请阅读 WebFlux 部分中的 响应式库)。 这包括 spring-webflux 中的 WebClient 和其他库,例如 Spring Data 响应式数据仓库。在这种情况下,能够从控制器方法返回 响应式类型很方便。

响应式返回值的处理方式如下:

  • 单值承诺的适配方式与使用 DeferredResult 类似。示例 包括 CompletionStage (JDK)、Mono (Reactor) 和 Single (RxJava)。

  • 具有流媒体类型(例如 application/x-ndjsontext/event-stream)的多值流的适配方式与使用 ResponseBodyEmitterSseEmitter 类似。示例包括 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 上下文传播存在于类路径中,当控制器方法 返回 响应式类型(例如 FluxMono)时,所有 已注册 io.micrometer.ThreadLocalAccessorThreadLocal 值, 都将使用 ThreadLocalAccessor 分配的键写入 Reactor Context 作为键值对。

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

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()) {
	// ...
}

提供了以下 ThreadLocalAccessor 实现:

  • LocaleContextThreadLocalAccessor — 通过 LocaleContextHolder 传播 LocaleContext

  • RequestAttributesThreadLocalAccessor — 通过 RequestContextHolder 传播 RequestAttributes

上述内容不会自动注册。您需要在启动时通过 ContextRegistry.getInstance() 注册它们。

有关更多详细信息,请参阅 Micrometer 上下文传播库的 文档

断开连接

Servlet API 不提供远程客户端断开连接的任何通知。 因此,在流式传输到响应时,无论是通过 SseEmitter 还是 响应式类型,定期发送数据很重要, 因为如果客户端已断开连接,则写入将失败。发送可以是空的(仅注释)SSE 事件,或任何其他 对方必须解释为心跳并忽略的数据。

或者,考虑使用具有内置心跳机制的 Web 消息解决方案(例如 STOMP over WebSocket 或带有 SockJS 的 WebSocket)。

配置

必须在 Servlet 容器级别启用异步请求处理功能。 MVC 配置还公开了异步请求的几个选项。

Servlet 容器

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

在 Java 配置中,当您使用 AbstractAnnotationConfigDispatcherServletInitializer 初始化 Servlet 容器时,这会自动完成。

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

Spring MVC

MVC 配置公开了异步请求处理的以下选项:

  • Java 配置:在 WebMvcConfigurer 上使用 configureAsyncSupport 回调。

  • XML 命名空间:在 <mvc:annotation-driven> 下使用 <async-support> 元素。

您可以配置以下内容:

  • 异步请求的默认超时值取决于 底层的 Servlet 容器,除非明确设置。

  • AsyncTaskExecutor 用于当使用 响应式类型 进行流式传输时进行阻塞写入,以及用于 执行从控制器方法返回的 Callable 实例。 默认使用的那个在负载下不适合生产环境。

  • DeferredResultProcessingInterceptor 实现和 CallableProcessingInterceptor 实现。

请注意,您还可以设置 DeferredResultResponseBodyEmitterSseEmitter 的默认超时值。对于 Callable,您可以使用 WebAsyncTask 提供超时值。