异步请求

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

处理

以下是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()

您也可以在ResponseEntity中使用ResponseBodyEmitter作为主体,从而可以自定义响应的状态和标头。

emitter抛出IOException(例如,如果远程客户端断开连接)时,应用程序无需负责清理连接,也不应调用emitter.completeemitter.completeWithError。相反,Servlet 容器会自动启动AsyncListener错误通知,其中 Spring MVC 会进行completeWithError调用。此调用反过来会执行对应用程序的最终ASYNC分派,在此期间 Spring MVC 将调用配置的异常解析器并完成请求。

SSE

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

您可以在ResponseEntity中使用StreamingResponseBody作为主体来自定义响应的状态和标头。

响应式类型

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

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

  • 单值承诺会被适配,类似于使用DeferredResult。示例包括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-coreReactiveAdapterRegistry支持 Reactor 和 RxJava,这使得它可以从多个响应式库进行适配。

对于流式传输到响应,支持响应式背压,但是对响应的写入仍然是阻塞的,并且通过配置的AsyncTaskExecutor在单独的线程上运行,以避免阻塞上游源,例如从WebClient返回的Flux

上下文传播

通常通过java.lang.ThreadLocal传播上下文。这在同一线程上处理时可以透明地工作,但是需要额外的工作才能跨多个线程进行异步处理。Micrometer 上下文传播库简化了跨线程以及跨上下文机制(例如ThreadLocal值、Reactor 上下文、GraphQL Java 上下文等)的上下文传播。

如果类路径上存在 Micrometer 上下文传播,当控制器方法返回响应式类型(例如FluxMono)时,所有ThreadLocal值(其中存在注册的io.micrometer.ThreadLocalAccessor)都将作为键值对写入 Reactor Context,使用ThreadLocalAccessor分配的键。

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

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 消息传递解决方案(例如基于 WebSocket 的 STOMP或带有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 容器,除非显式设置。

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

  • DeferredResultProcessingInterceptor实现和CallableProcessingInterceptor实现。

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