1. 概要

通常、アプリケーションでHTTPリクエストを行う場合、これらの呼び出しを順番に実行します。 ただし、これらのリクエストを同時に実行したい場合があります。

たとえば、複数のソースからデータを取得する場合や、単にアプリケーションのパフォーマンスを向上させたい場合に、これを実行したい場合があります。

このクイックチュートリアルでは、いくつかのアプローチを見て、SpringリアクティブWebClientを使用して並列サービス呼び出しを行うことでこれを実現する方法を確認します。

2. リアクティブプログラミングの要約

簡単に要約すると、 WebClientはSpring5で導入され、SpringWebReactiveモジュールの一部として含まれています。 HTTPリクエストを送信するためのリアクティブな非ブロッキングインターフェースを提供します

WebFluxを使用したリアクティブプログラミングの詳細なガイドについては、優れた Spring 5WebFluxガイドをご覧ください。

3. シンプルなユーザーサービス

この例では、単純な UserAPIを使用します。 このAPIには、IDをパラメーターとして使用してユーザーを取得するための1つのメソッドgetUserを公開するGETメソッドがあります

特定のIDのユーザーを取得するために1回の呼び出しを行う方法を見てみましょう。

WebClient webClient = WebClient.create("http://localhost:8080");
public Mono<User> getUser(int id) {
    LOG.info(String.format("Calling getUser(%d)", id));

    return webClient.get()
        .uri("/user/{id}", id)
        .retrieve()
        .bodyToMono(User.class);
}

次のセクションでは、このメソッドを同時に呼び出す方法を学習します。

4. WebClient呼び出しを同時に行う

このセクションでは、getUserメソッドを同時に呼び出すためのいくつかの例を示します。 また、例では、パブリッシャーの実装FluxMonoの両方についても見ていきます。

4.1. 同じサービスへの複数の呼び出し

5人のユーザーに関するデータを同時にフェッチし、その結果をユーザーのリストとして返したいとしましょう

public Flux fetchUsers(List userIds) {
    return Flux.fromIterable(userIds)
        .flatMap(this::getUser);
}

手順を分解して、これまでに行ったことを理解しましょう。

まず、静的な fromIterable メソッドを使用して、userIdsのリストからFluxを作成します。

次に、 flatMap を呼び出して、前に作成したgetUserメソッドを実行します。 このリアクティブ演算子の同時実行レベルはデフォルトで256です。つまり、最大256のgetUser呼び出しを同時に実行します。 この数は、オーバーロードされたバージョンのflatMapを使用してメソッドパラメーターを介して構成できます。

操作は並行して行われているため、結果の順序がわからないことに注意してください。 入力順序を維持する必要がある場合は、代わりにflatMapSequential演算子を使用できます。

Spring WebClientは内部で非ブロッキングHTTPクライアントを使用するため、ユーザーがスケジューラーを定義する必要はありません。 WebClient は、呼び出しのスケジュール設定と、ブロックせずに内部で適切なスレッドに結果を公開する処理を行います。

4.2. 同じタイプを返す異なるサービスへの複数の呼び出し

複数のサービスを同時に呼び出す方法を見てみましょう

この例では、同じUserタイプを返す別のエンドポイントを作成します。

public Mono<User> getOtherUser(int id) {
    return webClient.get()
        .uri("/otheruser/{id}", id)
        .retrieve()
        .bodyToMono(User.class);
}

ここで、2つ以上の呼び出しを並行して実行するメソッドは次のようになります。

public Flux fetchUserAndOtherUser(int id) {
    return Flux.merge(getUser(id), getOtherUser(id));
}

この例の主な違いは、fromIterableメソッドの代わりに静的メソッドmergeを使用したことです。 マージ方法を使用すると、2つ以上のFluxを1つの結果に組み合わせることができます。

4.3. さまざまなサービスへの複数の呼び出しさまざまなタイプ

2つのサービスが同じものを返す可能性はかなり低いです。 より一般的には、異なる応答タイプを提供する別のサービスがあり、目標は2つ(またはそれ以上)の応答をマージすることです

Mono クラスは、2つ以上の結果を組み合わせることができる静的zipメソッドを提供します。

public Mono fetchUserAndItem(int userId, int itemId) {
    Mono user = getUser(userId);
    Mono item = getItem(itemId);

    return Mono.zip(user, item, UserWithItem::new);
}

zipメソッドは、指定されたユーザーとアイテムのモノラルをタイプUserWithItemの新しいモノラルに結合します。 これは、ユーザーとアイテムをラップする単純なPOJOオブジェクトです。

5. テスト

このセクションでは、すでに見たコードをテストする方法を確認し、特に、サービス呼び出しが並行して行われていることを確認します。

このために、 Wiremock を使用してモックサーバーを作成し、fetchUsersメソッドをテストします。

@Test
public void givenClient_whenFetchingUsers_thenExecutionTimeIsLessThanDouble() {
        
    int requestsNumber = 5;
    int singleRequestTime = 1000;

    for (int i = 1; i <= requestsNumber; i++) {
        stubFor(get(urlEqualTo("/user/" + i)).willReturn(aResponse().withFixedDelay(singleRequestTime)
            .withStatus(200)
            .withHeader("Content-Type", "application/json")
            .withBody(String.format("{ \"id\": %d }", i))));
    }

    List<Integer> userIds = IntStream.rangeClosed(1, requestsNumber)
        .boxed()
        .collect(Collectors.toList());

    Client client = new Client("http://localhost:8089");

    long start = System.currentTimeMillis();
    List<User> users = client.fetchUsers(userIds).collectList().block();
    long end = System.currentTimeMillis();

    long totalExecutionTime = end - start;

    assertEquals("Unexpected number of users", requestsNumber, users.size());
    assertTrue("Execution time is too big", 2 * singleRequestTime > totalExecutionTime);
}

この例では、ユーザーサービスをモックして、1秒以内にすべてのリクエストに応答させるというアプローチを採用しています。 WebClientを使用して5回の呼び出しを行う場合、呼び出しは同時に発生するため、2秒以上かかることはないと想定できます

WebClient をテストするための他の手法については、SpringでのWebClientのモッキングに関するガイドをご覧ください。

6. 結論

このチュートリアルでは、 Spring 5ReactiveWebClientを使用してHTTPサービス呼び出しを同時に行うことができるいくつかの方法を検討しました。

まず、同じサービスと並行して電話をかける方法を示しました。 後で、異なるタイプを返す2つのサービスを呼び出す方法の例を見ました。 次に、モックサーバーを使用してこのコードをテストする方法を示しました。

いつものように、この記事のソースコードはGitHubで入手できます。