非同步請求
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
時,您可以選擇是否呼叫 setResult
或 setErrorResult
並帶有例外。在這兩種情況下,Spring MVC 都會將請求 dispatch 回 Servlet 容器以完成處理。然後,它會被視為控制器方法傳回給定值,或視為產生給定例外。然後,例外會通過常規例外處理機制 (例如,調用 @ExceptionHandler
方法)。
當您使用 Callable
時,會發生類似的處理邏輯,主要區別在於結果是從 Callable
傳回的,或是由此引發例外。
攔截
HandlerInterceptor
實例可以是 AsyncHandlerInterceptor
類型,以便在啟動非同步處理的初始請求上接收 afterConcurrentHandlingStarted
回呼 (而不是 postHandle
和 afterCompletion
)。
HandlerInterceptor
實作也可以註冊 CallableProcessingInterceptor
或 DeferredResultProcessingInterceptor
,以便更深入地與非同步請求的生命週期整合 (例如,處理逾時事件)。請參閱 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 串流
您可以將 DeferredResult
和 Callable
用於單一非同步傳回值。如果您想要產生多個非同步值,並將這些值寫入回應怎麼辦?本節說明如何執行此操作。
物件
您可以使用 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.complete
或 emitter.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-webflux
的 WebClient
和其他程式庫,例如 Spring Data 反應式資料儲存庫。在這種情況下,能夠從控制器方法傳回反應式類型會很方便。
反應式傳回值的處理方式如下
-
單一值 promise 會調整為類似於使用
DeferredResult
。範例包括Mono
(Reactor) 或Single
(RxJava)。 -
具有串流媒體類型 (例如
application/x-ndjson
或text/event-stream
) 的多值串流會調整為類似於使用ResponseBodyEmitter
或SseEmitter
。範例包括Flux
(Reactor) 或Observable
(RxJava)。應用程式也可以傳回Flux<ServerSentEvent>
或Observable<ServerSentEvent>
。 -
具有任何其他媒體類型 (例如
application/json
) 的多值串流會調整為類似於使用DeferredResult<List<?>>
。
Spring MVC 透過來自 spring-core 的 ReactiveAdapterRegistry 支援 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),例如 Flux
或 Mono
時,所有已註冊 io.micrometer.ThreadLocalAccessor
的 ThreadLocal
值,都會被寫入 Reactor Context
中作為鍵值對 (key-value pairs),並使用由 ThreadLocalAccessor
指定的鍵 (key)。
對於其他非同步處理情境,您可以直接使用 Context Propagation 程式庫。例如:
// 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>
新增至 DispatcherServlet
和 Filter
宣告,並將 <dispatcher>ASYNC</dispatcher>
新增至 filter mappings。
Spring MVC
MVC 組態設定公開了以下非同步請求處理的選項:
-
Java 組態設定:在
WebMvcConfigurer
上使用configureAsyncSupport
回呼 (callback)。 -
XML 命名空間:在
<mvc:annotation-driven>
下使用<async-support>
元素。
您可以設定以下項目:
-
非同步請求的預設逾時值取決於底層的 Servlet 容器,除非已明確設定。
-
AsyncTaskExecutor
用於在使用反應式類型串流時的阻塞式寫入,以及執行從控制器方法回傳的Callable
實例。預設使用的執行器不適用於生產環境的負載。 -
DeferredResultProcessingInterceptor
實作和CallableProcessingInterceptor
實作。
請注意,您也可以在 DeferredResult
、ResponseBodyEmitter
和 SseEmitter
上設定預設逾時值。對於 Callable
,您可以使用 WebAsyncTask
來提供逾時值。