非同步請求

Spring MVC 與 Servlet 非同步請求具有廣泛的整合處理

如需了解這與 Spring WebFlux 的差異之總覽,請參閱下方的Async 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 中的 forward,不同之處在於它讓應用程式在 Servlet 容器執行緒上恢復請求處理。

  • ServletRequest 提供對目前 DispatcherType 的存取,您可以使用它來區分處理初始請求、非同步 dispatch、forward 和其他 dispatcher 類型。

DeferredResult 處理的運作方式如下

  • 控制器傳回 DeferredResult 並將其儲存在某些記憶體內佇列或列表中,以便可以存取它。

  • Spring MVC 呼叫 request.startAsync()

  • 同時,DispatcherServlet 和所有組態的篩選器都會退出請求處理執行緒,但回應保持開啟。

  • 應用程式從某些執行緒設定 DeferredResult,而 Spring MVC 會將請求 dispatch 回 Servlet 容器。

  • 再次調用 DispatcherServlet,並使用非同步產生的傳回值恢復處理。

Callable 處理的運作方式如下

  • 控制器傳回 Callable

  • Spring MVC 呼叫 request.startAsync() 並將 Callable 提交至 AsyncTaskExecutor,以便在個別執行緒中處理。

  • 同時,DispatcherServlet 和所有篩選器都會退出 Servlet 容器執行緒,但回應保持開啟。

  • 最終,Callable 產生結果,而 Spring MVC 會將請求 dispatch 回 Servlet 容器以完成處理。

  • 再次調用 DispatcherServlet,並使用從 Callable 非同步產生的傳回值恢復處理。

如需更多背景資訊與內容,您也可以閱讀部落格文章,其中介紹了 Spring MVC 3.2 中的非同步請求處理支援。

例外處理

當您使用 DeferredResult 時,您可以選擇是否呼叫 setResultsetErrorResult 並帶有例外。在這兩種情況下,Spring MVC 都會將請求 dispatch 回 Servlet 容器以完成處理。然後,它會被視為控制器方法傳回給定值,或視為產生給定例外。然後,例外會通過常規例外處理機制 (例如,調用 @ExceptionHandler 方法)。

當您使用 Callable 時,會發生類似的處理邏輯,主要區別在於結果是從 Callable 傳回的,或是由此引發例外。

攔截

HandlerInterceptor 實例可以是 AsyncHandlerInterceptor 類型,以便在啟動非同步處理的初始請求上接收 afterConcurrentHandlingStarted 回呼 (而不是 postHandleafterCompletion)。

HandlerInterceptor 實作也可以註冊 CallableProcessingInterceptorDeferredResultProcessingInterceptor,以便更深入地與非同步請求的生命週期整合 (例如,處理逾時事件)。請參閱 AsyncHandlerInterceptor 以取得更多詳細資訊。

DeferredResult 提供 onTimeout(Runnable)onCompletion(Runnable) 回呼。請參閱 DeferredResult 的 javadoc 以取得更多詳細資訊。Callable 可以替換為 WebAsyncTask,後者公開了用於逾時與完成回呼的其他方法。

Async Spring MVC 與 WebFlux 比較

Servlet API 最初是為單次通過 Filter-Servlet 鏈而建置的。非同步請求處理讓應用程式可以退出 Filter-Servlet 鏈,但保持回應開啟以進行進一步處理。Spring MVC 非同步支援是圍繞該機制建置的。當控制器傳回 DeferredResult 時,Filter-Servlet 鏈會退出,並且 Servlet 容器執行緒會釋放。稍後,當設定 DeferredResult 時,會進行 ASYNC dispatch (到相同的 URL),在此期間會再次對應控制器,但不會調用它,而是使用 DeferredResult 值 (就像控制器傳回它一樣) 來恢復處理。

相反地,Spring WebFlux 既不是建置在 Servlet API 之上,也不需要此類非同步請求處理功能,因為它在設計上是非同步的。非同步處理已建置到所有 Framework 合約中,並且在請求處理的所有階段中都內在支援。

從程式設計模型的角度來看,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();
	// 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 中的 body,讓您可以自訂回應的狀態與標頭。

emitter 擲回 IOException (例如,如果遠端客户端已離開) 時,應用程式不負責清理連線,並且不應調用 emitter.completeemitter.completeWithError。相反地,servlet 容器會自動啟動 AsyncListener 錯誤通知,在其中 Spring MVC 進行 completeWithError 呼叫。此呼叫反過來會執行一個最終的 ASYNC dispatch 至應用程式,在此期間 Spring MVC 會調用組態的例外解析器並完成請求。

SSE

SseEmitter (ResponseBodyEmitter 的子類別) 提供對 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 fallback 傳輸 (包括 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 中的 body,以自訂回應的狀態與標頭。

反應式類型

Spring MVC 支援在控制器中使用反應式客户端程式庫 (另請閱讀 WebFlux 章節中的 反應式程式庫)。這包括來自 spring-webfluxWebClient 和其他程式庫,例如 Spring Data 反應式資料儲存庫。在這種情況下,能夠從控制器方法傳回反應式類型會很方便。

反應式傳回值的處理方式如下

  • 單一值 promise 會調整為類似於使用 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

Context 傳播

透過 java.lang.ThreadLocal 傳播 context 很常見。這對於在相同執行緒上處理來說是透明的,但對於跨多個執行緒的非同步處理則需要額外的工作。Micrometer Context Propagation 程式庫簡化了跨執行緒以及跨 context 機制 (例如 ThreadLocal 值、Reactor context、GraphQL Java context 和其他) 的 context 傳播。

如果 Micrometer Context Propagation 存在於類別路徑中,當控制器方法回傳反應式類型 (reactive type),例如 FluxMono 時,所有已註冊 io.micrometer.ThreadLocalAccessorThreadLocal 值,都會被寫入 Reactor Context 中作為鍵值對 (key-value pairs),並使用由 ThreadLocalAccessor 指定的鍵 (key)。

對於其他非同步處理情境,您可以直接使用 Context Propagation 程式庫。例如:

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 Context Propagation 程式庫的文件

斷線

Servlet API 在遠端用戶端斷線時不會提供任何通知。因此,當串流資料至回應時,無論是透過 SseEmitter反應式類型,定期傳送資料非常重要,因為如果用戶端已斷線,寫入將會失敗。傳送可以採用空 (僅註解) SSE 事件或任何其他資料的形式,讓另一方必須將其解釋為心跳訊號並忽略。

或者,您可以考慮使用具有內建心跳機制 (heartbeat mechanism) 的網頁訊息傳遞解決方案 (例如 STOMP over WebSocket 或搭配 SockJS 的 WebSocket)。

組態設定

非同步請求處理功能必須在 Servlet 容器層級啟用。MVC 組態設定也公開了多個非同步請求的選項。

Servlet 容器

Filter 和 Servlet 宣告具有 asyncSupported 旗標,需要將其設定為 true 才能啟用非同步請求處理。此外,應宣告 Filter 映射 (mappings) 以處理 ASYNC jakarta.servlet.DispatchType

在 Java 組態設定中,當您使用 AbstractAnnotationConfigDispatcherServletInitializer 初始化 Servlet 容器時,這會自動完成。

web.xml 組態設定中,您可以將 <async-supported>true</async-supported> 新增至 DispatcherServletFilter 宣告,並將 <dispatcher>ASYNC</dispatcher> 新增至 filter mappings。

Spring MVC

MVC 組態設定公開了以下非同步請求處理的選項:

  • Java 組態設定:在 WebMvcConfigurer 上使用 configureAsyncSupport 回呼 (callback)。

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

您可以設定以下項目:

  • 非同步請求的預設逾時值取決於底層的 Servlet 容器,除非已明確設定。

  • AsyncTaskExecutor 用於在使用反應式類型串流時的阻塞式寫入,以及執行從控制器方法回傳的 Callable 實例。預設使用的執行器不適用於生產環境的負載。

  • DeferredResultProcessingInterceptor 實作和 CallableProcessingInterceptor 實作。

請注意,您也可以在 DeferredResultResponseBodyEmitterSseEmitter 上設定預設逾時值。對於 Callable,您可以使用 WebAsyncTask 來提供逾時值。