建立軟體功能時,日常活動是從不同來源檢索資料並將其聚合到響應中。在微服務中,這些源通常是外部REST API。
在本教程中,我們將使用 Java 的CompletableFuture高效地並行地從多個外部 REST API 檢索資料。
為什麼在 REST 呼叫中使用並行性
讓我們想象一個場景,我們需要更新物件中的各個欄位,每個欄位值都來自外部REST 呼叫。一種替代方法是按順序呼叫每個 API 來更新每個欄位。
然而,等待一個 REST 呼叫完成才能啟動另一個 REST 呼叫會增加我們服務的響應時間。例如,如果我們呼叫兩個 API,每個 API 需要 5 秒,則總時間將至少為 10 秒,因為第二個呼叫需要等待第一個呼叫完成。
相反,我們可以並行呼叫所有 API,這樣總時間就是最慢的 REST 呼叫的時間。例如,一個呼叫需要 7 秒,另一個呼叫需要 5 秒。在這種情況下,我們將等待 7 秒,因為我們已經並行處理了所有內容,並且必須等待所有結果完成。
因此,並行性是減少服務響應時間、使其更具可擴充套件性並改善使用者體驗的絕佳替代方案。
使用CompletableFuture實現並行性
Java 中的CompletableFuture類是一個方便的工具,用於組合和執行不同的並行任務以及處理單個任務錯誤。
在以下部分中,我們將使用它為輸入列表中的每個物件組合並執行三個 REST 呼叫。
1.建立演示應用程式
讓我們首先定義更新的目標POJO :
public class Purchase { |
Purchase類 具有三個應更新的欄位,每個欄位均由 ID 查詢的不同 REST 呼叫進行更新。
我們首先建立一個類,定義一個 RestTemplate bean 和一個 用於 REST 呼叫的域URL :
@Component |
現在,讓我們定義 / orders API 呼叫:
public String getOrderDescription(String orderId) { |
然後,讓我們定義 / payment API 呼叫:
public String getPaymentDescription(String paymentId) { |
最後,我們定義/users API 呼叫:
public String getUserName(String userId) { |
所有三個方法都使用getForEntity()方法進行 REST 呼叫並將結果包裝在ResponseEntity物件中。
然後,我們呼叫getBody()從 REST 呼叫中獲取響應正文。
2.使用CompletableFuture進行多個 REST 呼叫
現在,讓我們建立構建並執行一組三個CompletableFuture的方法:
public void updatePurchase(Purchase purchase) { |
我們使用 allOf()方法來構建CompletableFuture的步驟 。每個引數都是一個並行任務,其形式是使用 REST 呼叫及其結果構建的另一個CompletableFuture 。
為了構建每個並行任務,我們首先使用SupplyAsync()方法來提供 供應商,我們將從中檢索資料。然後,我們使用thenAccept()來使用SupplyAsync()的結果 並將其設定到Purchasing類中的相應欄位上。
在allOf()的末尾,我們剛剛構建了任務。沒有采取任何行動。
最後,我們在最後呼叫join()來並行執行所有任務並收集它們的結果。由於join()是一個執行緒阻塞操作,因此我們只在最後呼叫它,而不是在每個任務步驟中呼叫它。這是為了透過減少執行緒塊來最佳化應用程式效能。
由於我們沒有為 SupplyAsync()方法提供自定義的ExecutorService,因此所有任務都在同一個執行器中執行。預設情況下,Java 使用ForkJoinPool.commonPool()。
一般來說,為 SupplyAsync()指定自定義ExecutorService是一個很好的做法,這樣我們就可以更好地控制執行緒池引數。
3.對列表中的每個元素執行多個 REST 呼叫
要將updatePurchase()方法應用於集合,我們可以簡單地在forEach()迴圈中呼叫它:
public void updatePurchases(List<Purchase> purchases) { |
我們的updatePurchase()方法接收Purchasing列表,並將之前建立的updatePurchase()方法應用於每個元素。
每次呼叫updatePurchases()都會執行CompletableFuture中定義的三個並行任務。因此,每次購買都有自己的 CompletableFuture物件來執行三個並行 REST 呼叫。
處理錯誤
在分散式系統中,服務不可用或網路故障是很常見的。這些故障可能發生在外部 REST API 中,而我們作為該 API 的客戶端並不知道。例如,如果應用程式關閉,則透過線路傳送的請求永遠不會完成。
1.使用handle()優雅地處理錯誤
REST呼叫執行期間可能會出現異常。例如,如果 API 服務關閉或者我們輸入無效引數,我們就會收到錯誤訊息。
因此,我們可以使用handle()方法單獨處理每個REST呼叫異常:
public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) |
方法引數是一個BiFunction,包含前一個任務的結果和異常作為引數。
為了說明這一點,讓我們將 handle()步驟新增到CompletableFuture的步驟之一中 :
public void updatePurchaseHandlingExceptions(Purchase purchase) { |
在示例中, handle ()從thenAccept()呼叫的setOrderDescription()獲取Void型別。
然後,它將thenAccept()操作中丟擲的任何錯誤儲存在異常中。因此,我們用它來檢查錯誤並在if語句中正確處理它。
最後, 如果沒有丟擲異常, handle()將返回作為引數傳遞的值。否則,它返回 null。
2.處理 REST 呼叫超時
當我們使用 CompletableFuture時,我們可以指定一個類似於我們在 REST 呼叫中定義的任務超時。因此,如果任務未在指定時間內完成,Java 將透過 TimeoutException 結束任務執行。
為此,我們修改CompletableFuture 的一項任務來處理超時:
public void updatePurchaseHandlingExceptions(Purchase purchase) { |
我們已將 orTimeout()行新增到 CompletableFuture構建器中,以便在5秒內未完成任務時突然停止任務執行。
我們還在handle()方法中 新增了一個if語句來單獨處理 TimeoutException 。
向CompletableFuture新增超時可確保任務始終完成。這對於避免執行緒無限期掛起、等待可能永遠不會完成的操作的結果非常重要。因此,它減少了長時間處於RUNNING狀態的執行緒數量並提高了應用程式的執行狀況。