1. 概述
長輪詢是一種服務器應用程序使用的技術,用於在信息可用時保持客户端連接。 這通常在服務器必須調用下游服務以獲取信息並等待結果時使用。
在本教程中,我們將通過使用 DeferredResult 來探索 Spring MVC 中長輪詢的概念。 我們將首先查看一個基本實現,使用 DeferredResult,然後討論如何處理錯誤和超時。 最後,我們將探討如何進行測試。
2. 使用 DeferredResult 進行長輪詢
我們可以使用 DeferredResult 在 Spring MVC 中作為一種處理異步傳入 HTTP 請求的方式。它允許 HTTP 工作線程被釋放,以便處理其他傳入的請求,並將工作卸載到另一個工作線程。 這樣有助於服務對需要長時間計算或任意等待時間的請求具有可用性。
我們之前的關於 Spring 的 DeferredResult 類的文章更深入地介紹了它的功能和用例。
2.1. 發佈者
讓我們通過創建一個使用 DeferredResult 的發佈應用程序來開始我們的長輪詢示例。
首先,讓我們定義一個使用 @RestController 並且不將工作卸載到另一個工作線程的 Spring 應用程序:
@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"));它接收一個可執行對象作為輸入——當超時閾值達到時,由容器線程調用。
如果超時閾值達到,則將其視為錯誤,並使用設置錯誤結果。
2.2. 訂閲者
現在我們已經設置好發佈應用程序,接下來我們將編寫一個訂閲客户端應用程序。
編寫一個調用此長輪詢 API 的服務相當簡單,因為它本質上與為標準阻塞 REST 調用編寫客户端相同。唯一的真正差異是,由於長輪詢的等待時間,我們希望確保在位有一個超時機制。 在 Spring MVC 中,我們可以使用 RestTemplate 或 WebClient 來實現這一點,因為兩者都具有內置的超時處理功能。
首先,讓我們從使用 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. 長期輪詢測試
現在我們已經啓動並運行了應用程序,接下來我們來討論如何進行測試。 我們可以首先使用 MockMvcMvcResult 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 的超時機制,我們需要稍微修改測試代碼,在 asyncListener 和 response 調用之間添加一個超時啓用器。
((MockAsyncContext) asyncListener
.getRequest()
.getAsyncContext())
.getListeners()
.get(0)
.onTimeout(null);這段代碼可能看起來有些奇怪,但我們之所以在 onTimeout 中使用這個名稱,是為了通知 AsyncListener 一個操作超時了。 這將確保我們為 onTimeout 方法實現的 Runnable 類在控制器中被正確調用。
4. 結論
本文介紹了在長輪詢場景下如何使用 DeferredResult。我們還討論瞭如何編寫訂閲客户端以及如何進行測試。