异步请求

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<>();
	// 将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时,您可以选择调用setResultsetErrorResult并传入异常。在这两种情况下,Spring MVC将请求重新分派回Servlet容器以完成处理。然后,它会被视为控制器方法返回给定值或产生给定异常。异常随后通过常规异常处理机制(例如,调用@ExceptionHandler方法)处理。

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

拦截

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

HandlerInterceptor实现还可以注册CallableProcessingInterceptorDeferredResultProcessingInterceptor,以更深入地集成异步请求的生命周期(例如,处理超时事件)。有关更多详细信息,请参阅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流

您可以使用DeferredResultCallable来返回单个异步值。如果您想要生成多个异步值并将其写入响应怎么办?本节描述了如何实现此目的。

对象

您可以使用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.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();
	// 将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-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)。

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:

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

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 on WebMvcConfigurer.

  • 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 executing Callable instances returned from controller methods. The one used by default is not suitable for production under load.

  • DeferredResultProcessingInterceptor implementations and CallableProcessingInterceptor 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.