CompletableFuture中實現多個 REST 呼叫

banq發表於2024-05-19

建立軟體功能時,日常活動是從不同來源檢索資料並將其聚合到響應中。在微服務中,這些源通常是外部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 {
    String orderDescription;
    String paymentDescription;
    String buyerName;
    String orderId;
    String paymentId;
    String userId;
    <font>// all-arg constructor, getters and setters<i>
}

Purchase類 具有三個應更新的欄位,每個欄位均由 ID 查詢的不同 REST 呼叫進行更新。

我們首先建立一個類,定義一個 RestTemplate bean 和一個 用於 REST 呼叫的域URL :

@Component
public class PurchaseRestCallsAsyncExecutor {
    RestTemplate restTemplate;
    static final String BASE_URL = <font>"https://internal-api.com";
   
// all-arg constructor<i>
}

現在,讓我們定義 / orders API 呼叫:

public String getOrderDescription(String orderId) {
    ResponseEntity<String> result = restTemplate.getForEntity(String.format(<font>"%s/orders/%s", BASE_URL, orderId),
        String.class);
    return result.getBody();
}

然後,讓我們定義 / payment API 呼叫:

public String getPaymentDescription(String paymentId) {
    ResponseEntity<String> result = restTemplate.getForEntity(String.format(<font>"%s/payments/%s", BASE_URL, paymentId),
        String.class);
    return result.getBody();
}

最後,我們定義/users API 呼叫:

public String getUserName(String userId) {
    ResponseEntity<String> result = restTemplate.getForEntity(String.format(<font>"%s/users/%s", BASE_URL, userId),
        String.class);
    return result.getBody();
}

所有三個方法都使用getForEntity()方法進行 REST 呼叫並將結果包裝在ResponseEntity物件中。

然後,我們呼叫getBody()從 REST 呼叫中獲取響應正文。

2.使用CompletableFuture進行多個 REST 呼叫
現在,讓我們建立構建並執行一組三個CompletableFuture的方法:

public void updatePurchase(Purchase purchase) {
    CompletableFuture.allOf(
      CompletableFuture.supplyAsync(() -> getOrderDescription(purchase.getOrderId()))
        .thenAccept(purchase::setOrderDescription),
      CompletableFuture.supplyAsync(() -> getPaymentDescription(purchase.getPaymentId()))
        .thenAccept(purchase::setPaymentDescription),
      CompletableFuture.supplyAsync(() -> getUserName(purchase.getUserId()))
        .thenAccept(purchase::setBuyerName)
    ).join();
}

我們使用 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) {
    purchases.forEach(this::updatePurchase);
}

我們的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) {
    CompletableFuture.allOf(
        CompletableFuture.supplyAsync(() -> getPaymentDescription(purchase.getPaymentId()))
          .thenAccept(purchase::setPaymentDescription)
          .handle((result, exception) -> {
              if (exception != null) {
                  <font>// handle exception<i>
                  return null;
              }
              return result;
          })
    ).join();
}

在示例中,  handle ()從thenAccept()呼叫的setOrderDescription()獲取Void型別。

然後,它將thenAccept()操作中丟擲的任何錯誤儲存在異常中。因此,我們用它來檢查錯誤並在if語句中正確處理它。

最後, 如果沒有丟擲異常, handle()將返回作為引數傳遞的值。否則,它返回 null。

2.處理 REST 呼叫超時
當我們使用 CompletableFuture時,我們可以指定一個類似於我們在 REST 呼叫中定義的任務超時。因此,如果任務未在指定時間內完成,Java 將透過 TimeoutException 結束任務執行。

為此,我們修改CompletableFuture 的一項任務來處理超時:

public void updatePurchaseHandlingExceptions(Purchase purchase) {
    CompletableFuture.allOf(
        CompletableFuture.supplyAsync(() -> getOrderDescription(purchase.getOrderId()))
          .thenAccept(purchase::setOrderDescription)
          .orTimeout(5, TimeUnit.SECONDS)
          .handle((result, exception) -> {
              if (exception instanceof TimeoutException) {
                  <font>// handle exception<i>
                  return null;
              }
              return result;
          })
    ).join();
}

我們已將 orTimeout()行新增到 CompletableFuture構建器中,以便在5秒內未完成任務時突然停止任務執行。

我們還在handle()方法中 新增了一個if語句來單獨處理 TimeoutException  。

向CompletableFuture新增超時可確保任務始終完成。這對於避免執行緒無限期掛起、等待可能永遠不會完成的操作的結果非常重要。因此,它減少了長時間處於RUNNING狀態的執行緒數量並提高了應用程式的執行狀況。

相關文章