Spring MVC中的長輪詢

1.概述

長輪詢是服務器應用程序用來保持客戶端連接直到信息可用的一種方法。當服務器必須調用下游服務以獲取信息並等待結果時,通常使用此方法。

在本教程中,我們將使用DeferredResult .我們先來看一個基本的實現 使用DeferredResult ,然後討論如何處理錯誤和超時。最後,我們將研究如何測試所有這些。

2.使用DeferredResult

我們可以DeferredResult作為異步處理入站HTTP請求的方法。它允許釋放HTTP工作線程來處理其他傳入請求,並將工作分流到另一個工作線程。這樣,對於需要長時間計算或任意等待時間的請求,它有助於提高服務可用性。

我們之前關於Spring的DeferredResult類的文章更深入地介紹了其功能和用例。

2.1。發行人

DeferredResult.的發布應用程序開始我們的長輪詢示例。

最初,讓我們定義一個Spring @RestController ,它使用DeferredResult但不會將其工作分流到另一個工作線程:

@RestController

 @RequestMapping("/api")

 public class BakeryController {

 @GetMapping("/bake/{bakedGood}")

 public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {

 DeferredResult<String> output = new DeferredResult<>();

 try {

 Thread.sleep(bakeTime);

 output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));

 } catch (Exception e) {

 // ...

 }

 return output;

 }

 }

該控制器以與常規阻塞控制器相同的方式同步工作。這樣,我們的HTTP線程將被完全阻止,直到通過bakeTime如果我們的服務有很多入站流量,這是不理想的。

現在,通過將工作卸載到工作線程來異步設置輸出:

private ExecutorService bakers = Executors.newFixedThreadPool(5);



 @GetMapping("/bake/{bakedGood}")

 public DeferredResult<String> publisher(@PathVariable String bakedGood, @RequestParam Integer bakeTime) {

 DeferredResult<String> output = new DeferredResult<>();

 bakers.execute(() -> {

 try {

 Thread.sleep(bakeTime);

 output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));

 } catch (Exception e) {

 // ...

 }

 });

 return output;

 }

在此示例中,我們現在可以釋放HTTP工作線程來處理其他請求。 bakers池中的工作線程正在執行工作,並將在完成時設置結果。當工作程序調用setResult ,它將允許容器線程響應調用方客戶端。

現在,我們的代碼非常適合長時間輪詢,並且與傳統的阻塞控制器相比,我們的服務可更適合入站HTTP請求使用。但是,我們還需要注意一些邊緣情況,例如錯誤處理和超時處理。

為了處理工作人員拋出的檢查錯誤,我們將使用DeferredResult提供setErrorResult方法:

bakers.execute(() -> {

 try {

 Thread.sleep(bakeTime);

 output.setResult(format("Bake for %s complete and order dispatched. Enjoy!", bakedGood));

 } catch (Exception e) {

 output.setErrorResult("Something went wrong with your order!");

 }

 });

現在,工作線程可以正常處理所有引發的異常。

由於通常採用長輪詢來異步和同步地處理來自下游系統的響應,因此在我們從未收到來自下游系統的響應的情況下,我們應該添加一種機制來強制執行超時。 DeferredResult API提供了一種執行此操作的機制。 DeferredResult對象的構造函數中傳入一個超時參數:

DeferredResult<String> output = new DeferredResult<>(5000L);

接下來,讓我們實現超時方案。為此,我們將使用onTimeout:

output.onTimeout(() -> output.setErrorResult("the bakery is not responding in allowed time"));

這以Runnable作為輸入-當達到超時閾值時,它由容器線程調用。如果達到超時,則將其作為錯誤處理,並相應地使用setErrorResult .

2.2。訂戶

現在我們已經設置了發布應用程序,讓我們編寫一個訂閱客戶端應用程序。

編寫調用此長輪詢API的服務非常簡單,因為它與編寫用於標準阻止REST調用的客戶端基本相同。唯一的實際區別是,由於長輪詢的等待時間,我們要確保有適當的超時機制。在Spring MVC中,我們都可以使用RestTemplateWebClient來實現此目的,因為它們都具有內置的超時處理功能。

首先,讓我們從使用RestTemplate.讓我們使用RestTemplateBuilder RestTemplate的實例,以便我們可以設置超時時間:

public String callBakeWithRestTemplate(RestTemplateBuilder restTemplateBuilder) {

 RestTemplate restTemplate = restTemplateBuilder

 .setConnectTimeout(Duration.ofSeconds(10))

 .setReadTimeout(Duration.ofSeconds(10))

 .build();



 try {

 return restTemplate.getForObject("/api/bake/cookie?bakeTime=1000", String.class);

 } catch (ResourceAccessException e) {

 // handle timeout

 }

 }

在這段代碼中,通過ResourceAccessException ,我們能夠在超時時處理錯誤。

接下來,讓我們使用WebClient創建一個示例以實現相同的結果:

public String callBakeWithWebClient() {

 WebClient webClient = WebClient.create();

 try {

 return webClient.get()

 .uri("/api/bake/cookie?bakeTime=1000")

 .retrieve()

 .bodyToFlux(String.class)

 .timeout(Duration.ofSeconds(10))

 .blockFirst();

 } catch (ReadTimeoutException e) {

 // handle timeout

 }

 }

我們之前的有關設置Spring REST超時的文章更深入地介紹了該主題。

3.測試長輪詢

現在我們已經啟動了應用程序並正在運行,讓我們討論如何測試它。我們可以從使用MockMvc開始測試對控制器類的調用:

MvcResult asyncListener = mockMvc

 .perform(MockMvcRequestBuilders.get("/api/bake/cookie?bakeTime=1000"))

 .andExpect(request().asyncStarted())

 .andReturn();

在這裡,我們正在調用DeferredResult端點,並斷言該請求已啟動異步調用。從這裡開始,測試將等待異步結果的完成,這意味著我們不需要在測試中添加任何等待邏輯。

接下來,我們要斷言異步調用何時返回並且它與我們期望的值匹配:

String response = mockMvc

 .perform(asyncDispatch(asyncListener))

 .andReturn()

 .getResponse()

 .getContentAsString();



 assertThat(response)

 .isEqualTo("Bake for cookie complete and order dispatched. Enjoy!");

通過使用asyncDispatch() ,我們可以獲得異步調用的響應並聲明其值。

DeferredResult的超時機制asyncListenerresponse調用之間添加超時啟用程序來稍微更改測試代碼:

((MockAsyncContext) asyncListener

 .getRequest()

 .getAsyncContext())

 .getListeners()

 .get(0)

 .onTimeout(null);

這段代碼可能看起來很奇怪,但是我們以這種方式onTimeout我們這樣做是為了讓[AsyncListener](https://docs.oracle.com/javaee/7/api/javax/servlet/AsyncListener.html)知道某個操作已超時。這將確保正確調用我們為控制器中的onTimeout方法實現Runnable

4.結論

在本文中,我們介紹瞭如何在長輪詢的上下文中DeferredResult我們還討論瞭如何編寫用於長期輪詢的訂閱客戶端,以及如何對其進行測試。