天天看點

傳統的Spring Web MVC如何像WebFlux一樣異步處理請求?

文章目錄

  • ​​寫在前面​​
  • ​​一、DeferredResult​​
  • ​​二、Callable​​
  • ​​三、執行過程​​
  • ​​1、異常處理​​
  • ​​2、攔截​​
  • ​​3、與WebFlux的比較​​
  • ​​四、HTTP流​​
  • ​​1、響應多個對象​​
  • ​​2、SSE​​
  • ​​3、原始資料​​
  • ​​五、Reactive類型​​
  • ​​六、斷開連接配接​​
  • ​​七、配置​​
  • ​​1、servlet容器​​
  • ​​2、spring MVC​​
  • ​​參考資料​​

寫在前面

Spring MVC與Servlet 3.0異步請求處理進行了廣泛的內建:

  • 控制器方法中的DeferredResult和 Callable傳回值為單個異步傳回值提供了基本支援。
  • 控制器可以傳輸多個值,包括SSE和原始資料。
  • 控制器可以使用反應式用戶端并傳回反應式類型進行響應處理。

一、DeferredResult

一旦在Servlet容器中啟用了異步請求處理功能,控制器方法就可以用DeferredResult包裝任何受支援的控制器方法傳回值,如下例所示:

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// From some other thread...
deferredResult.setResult(result);      

控制器可以從不同的線程異步生成傳回值,例如,響應外部事件(JMS消息)、計劃任務或其他事件。

二、Callable

控制器可以用java.util.concurrent.Callable包裝任何支援的傳回值,如下例所示:

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };
}      

然後,可以通過配置的TaskExecutor運作給定的任務來獲得傳回值。

三、執行過程

下面是Servlet異步請求處理的一個非常簡明的概述:

  • 可以通過調用request.startAsync()将ServletRequest置于異步模式。這樣做的主要效果是Servlet(以及任何過濾器)可以退出,但是響應保持打開,讓處理稍後完成。
  • 對request.startAsync()的調用傳回AsyncContext,您可以使用它來進一步控制異步處理。例如,它提供了dispatch方法,類似于Servlet API的forward,隻是它讓應用程式恢複Servlet容器線程上的請求處理。
  • ServletRequest提供對目前DispatcherType的通路,您可以使用它來區分處理初始請求、異步排程、轉發和其他排程程式類型。

DeferredResult 處理的工作方式如下:

  • 控制器傳回一個DeferredResult,并将其儲存在某個可以通路的記憶體隊列或清單中。
  • Spring MVC調用request.startAsync()。
  • 同時,DispatcherServlet和所有配置的過濾器退出請求處理線程,但是響應保持打開。
  • 應用程式從某個線程設定DeferredResult,Spring MVC将請求發送回Servlet容器。
  • DispatcherServlet再次被調用,處理繼續進行,并傳回異步産生的值。

Callable 處理的工作方式如下:

  • 控制器傳回一個Callable。
  • Spring MVC調用request.startAsync()并将可調用内容送出給TaskExecutor,以便在單獨的線程中進行處理。
  • 同時,DispatcherServlet和所有過濾器退出Servlet容器線程,但是響應保持打開。
  • 最後,Callable産生一個結果,Spring MVC将請求發送回Servlet容器以完成處理。
  • DispatcherServlet再次被調用,處理繼續進行,并從Callable傳回值。

1、異常處理

使用DeferredResult時,可以選擇是調用setResult還是帶有異常的setErrorResult。在這兩種情況下,Spring MVC都将請求發送回Servlet容器以完成處理。然後,它要麼被視為控制器方法傳回了給定值,要麼被視為産生了給定的異常。然後異常通過正常的異常處理機制(例如,調用@ExceptionHandler方法)。

當您使用Callable時,會發生類似的處理邏輯,主要差別在于結果是從Callable傳回的,或者是由Callable引發的異常。

2、攔截

HandlerInterceptor執行個體可以是AsyncHandlerInterceptor類型,以便在啟動異步處理的初始請求上接收afterConcurrentHandlingStarted回調(而不是postHandle和afterCompletion)。

HandlerInterceptor實作還可以注冊CallableProcessingInterceptor或DeferredResultProcessingInterceptor,以便更深入地與異步請求的生命周期內建(例如,處理逾時事件)。請多請了解AsyncHandlerInterceptor。

DeferredResult提供onTimeout(Runnable)和onCompletion(Runnable)回調。有關更多詳細資訊,請參見DeferredResult的javadoc。Callable可以替代WebAsyncTask,後者公開逾時和完成回調的附加方法。

3、與WebFlux的比較

Servlet API最初是為通過Filter-Servlet鍊進行一次傳遞而建構的。Servlet 3.0中添加的異步請求處理允許應用程式退出Filter-Servlet鍊,但保持響應開放以供進一步處理。Spring MVC異步支援就是圍繞這一機制建構的。當控制器傳回DeferredResult時,Filter-Servlet鍊退出,Servlet容器線程被釋放。稍後,當設定了DeferredResult時,将進行異步排程(到同一個URL),在此期間,控制器将再次映射,但不是調用它,而是使用DeferredResult值(就像控制器傳回它一樣)來恢複處理。

相比之下,Spring WebFlux既不是建立在Servlet API之上,也不需要這樣的異步請求處理特性,因為它在設計上就是異步的。異步處理内置于所有架構契約中,并且在請求處理的所有階段都得到本質上的支援。

從程式設計模型的角度來看,Spring MVC和Spring WebFlux都支援異步和反應類型作為控制器方法中的傳回值。Spring MVC甚至支援流,包括反作用背壓。然而,對響應的單獨寫入保持阻塞(并且在單獨的線程上執行),這與WebFlux不同,web flux依賴于非阻塞I/O,并且每次寫入都不需要額外的線程。

另一個基本差別是,Spring MVC不支援控制器方法參數中的異步或反應類型(例如,@RequestBody,@RequestPart等),也不明确支援異步和反應類型作為模型屬性。Spring WebFlux确實支援所有這些。

四、HTTP流

對于單個異步傳回值,可以使用DeferredResult和Callable。如果您想産生多個異步值并将它們寫入響應,該怎麼辦?

1、響應多個對象

可以使用ResponseBodyEmitter傳回值生成對象流,其中每個對象都用HttpMessageConverter序列化并寫入響應,如下例所示:

@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();      

還可以使用ResponseBodyEmitter作為ResponseEntity中的主體,進而允許您自定義響應的狀态和标頭。

當發射器抛出IOException時(例如,如果遠端用戶端離開了),應用程式不負責清理連接配接,也不應該調用emitter.complete或emitter.completeWithError。相反,servlet容器會自動啟動AsyncListener錯誤通知,其中Spring MVC會調用completeWithError。這個調用反過來對應用程式執行最後一次異步排程,在此期間,Spring MVC調用配置好的異常解析器并完成請求。

2、SSE

SSE emitter(ResponseBodyEmitter的子類)提供了對伺服器發送事件的支援,其中從伺服器發送的事件根據W3C SSE規範進行格式化。若要從控制器生成SSE流,請傳回SseEmitter,如下例所示:

@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();      

雖然SSE是流入浏覽器的主要選項,但請注意,Internet Explorer不支援伺服器發送的事件。考慮将Spring的WebSocket消息傳遞與SockJS回退傳輸(包括SSE)結合使用,以廣泛的浏覽器為目标。

3、原始資料

有時,繞過消息轉換并直接流式傳輸到響應輸出流是有用的(例如,對于檔案下載下傳)。您可以使用StreamingResponseBody傳回值類型來實作這一點,如下例所示:

@GetMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // write...
        }
    };
}      

可以使用StreamingResponseBody作為ResponseEntity中的正文來自定義響應的狀态和标頭。

五、Reactive類型

Spring MVC支援在控制器中使用反應式用戶端庫(也可以在WebFlux部分閱讀反應式庫)。這包括spring-webflux的WebClient和其他的,比如Spring Data reactive資料倉庫。在這種情況下,能夠從控制器方法傳回反應類型是很友善的。

Reactive傳回值的處理如下:

  • 單值promise 适用于,類似于使用DeferredResult。示例包括Mono(Reactor)或Single (RxJava)。
  • 與使用ResponseBodyEmitter或SseEmitter類似,具有流媒體類型(如application/x-ndjson或text/event-stream)适用于multi-value多值流。
  • 适應任何其他媒體類型(如application/json)的多值流,類似于使用DeferredResult<List <?>>.

Spring MVC通過來自spring-core的ReactiveAdapterRegistry支援Reactor和RxJava,這讓它可以适應多個反應庫。

對于到響應的流,支援反應性背壓,但是對響應的寫入仍然被阻塞,并且通過配置的TaskExecutor在單獨的線程上運作,以避免阻塞上遊源(例如從WebClient傳回的流量)。預設情況下,SimpleAsyncTaskExecutor用于阻塞寫操作,但這不适合負載情況。如果您計劃使用反應型的流,那麼您應該使用MVC配置來配置任務執行器。

六、斷開連接配接

當遠端用戶端離開時,Servlet API不提供任何通知。是以,在通過SseEmitter或reactive類型流式傳輸響應時,定期發送資料非常重要,因為如果用戶端斷開連接配接,寫入将會失敗。發送可以采用空(僅注釋)SSE事件或任何其他資料的形式,另一端必須将其解釋為心跳并忽略。

或者,考慮使用具有内置心跳機制的web消息傳遞解決方案(如STOMP over WebSocket或帶有SockJS的WebSocket)。

七、配置

異步請求處理特性必須在Servlet容器級别啟用。MVC配置還為異步請求提供了幾個選項。

1、servlet容器

Filter和Servlet聲明有一個asyncSupported标志,需要設定為true才能啟用異步請求處理。此外,應該聲明過濾器映射來處理ASYNC javax.servlet.DispatchType。

在Java配置中,當您使用AbstractAnnotationConfigDispatcherServletInitializer初始化Servlet容器時,這是自動完成的。

在web.xml配置中,可以将<async-supported>true</async-supported>添加到DispatcherServlet和篩選器聲明中,并将< dispatcher>ASYNC</dispatcher >添加到篩選器映射中。

2、spring MVC

MVC配置公開了以下與異步請求處理相關的選項:

  • Java配置:使用WebMvcConfigurer上的configureAsyncSupport回調。
  • XML名稱空間:使用< mvc:annotation-driven >下的< async-support >元素。

可以配置以下内容:

  • 異步請求的預設逾時值,如果沒有設定,則取決于底層Servlet容器。
  • AsyncTaskExecutor用于在使用反應類型進行流式處理時阻止寫入,以及用于執行從控制器方法傳回的Callable 執行個體。如果您使用反應類型或具有傳回Callable的控制器方法,我們強烈建議您配置此屬性,因為預設情況下,它是SimpleAsyncTaskExecutor。
  • DeferredResultProcessingInterceptor實作和CallableProcessingInterceptor實作。

參考資料