リアクティブJAX-RSクライアントAPI

リアクティブJAX-RSクライアントAPI

1. 前書き

このチュートリアルでは、Jersey APIを使用したリアクティブ(Rx)プログラミングのJAX-RSサポートについて説明します。 この記事は、読者がJersey RESTクライアントAPIの知識があることを前提としています。

reactive programming conceptsにある程度精通していると役立ちますが、必須ではありません。

2. 依存関係

まず、標準のJerseyクライアントライブラリの依存関係が必要です。


    org.glassfish.jersey.core
    jersey-client
    2.27


    org.glassfish.jersey.inject
    jersey-hk2
    2.27

これらの依存関係により、ストックJAX-RSリアクティブプログラミングサポートが提供されます。 jersey-clientおよびjersey-hk2の現在のバージョンは、MavenCentralで入手できます。

サードパーティのリアクティブフレームワークのサポートには、次の拡張機能を使用します。


    org.glassfish.jersey.ext.rx
    jersey-rx-client-rxjava
    2.27

上記の依存関係は、次の拡張子を使用する新しいRxJava2のFlowableに対してRxJavaのObservable;をサポートします。


    org.glassfish.jersey.ext.rx
    jersey-rx-client-rxjava2
    2.27

rxjavaおよびrxjava2への依存関係は、MavenCentralでも利用できます。

3. リアクティブJAX-RSクライアントが必要な理由

使用するRESTAPIが3つあるとします。

  • id-serviceは、長いユーザーIDのリストを提供します

  • name-serviceは、特定のユーザーIDのユーザー名を提供します

  • hash-serviceは、ユーザーIDとユーザー名の両方のハッシュを返します

私たちは、各サービスのクライアントを作成します。

Client client = ClientBuilder.newClient();
WebTarget userIdService = client.target("http://localhost:8080/id-service/ids");
WebTarget nameService
  = client.target("http://localhost:8080/name-service/users/{userId}/name");
WebTarget hashService = client.target("http://localhost:8080/hash-service/{rawValue}");

これは不自然な例ですが、説明のために機能します。 JAX-RS仕様は、これらのサービスを一緒に使用するための少なくとも3つのアプローチをサポートしています。

  • 同期(ブロッキング)

  • 非同期(非ブロッキング)

  • リアクティブ(機能的、ノンブロッキング)

3.1. 同期Jerseyクライアント呼び出しの問題

これらのサービスを利用するためのバニラアプローチでは、id-serviceを利用してユーザーIDを取得し、返されたIDごとにname-serviceおよびhash-serviceAPIを順番に呼び出します。

With this approach, each call blocks the running thread until the request is fulfilled、結合された要求を満たすために合計で多くの時間を費やします。 これは、重要なユースケースでは明らかに満足できるものではありません。

3.2. 非同期Jerseyクライアント呼び出しの問題

より洗練されたアプローチは、JAX-RSでサポートされているInvocationCallback メカニズムを使用することです。 最も基本的な形式では、getメソッドにコールバックを渡して、特定のAPI呼び出しが完了したときに何が起こるかを定義します。

これで真の非同期実行(with some limitations on thread efficiency)が得られましたが、些細なシナリオ以外でthis style of code can get unreadable and unwieldyがどのように実行されるかを簡単に確認できます。 JAX-RS specificationは、このシナリオをPyramid of Doomとして具体的に強調しています。

// used to keep track of the progress of the subsequent calls
CountDownLatch completionTracker = new CountDownLatch(expectedHashValues.size());

userIdService.request()
  .accept(MediaType.APPLICATION_JSON)
  .async()
  .get(new InvocationCallback>() {
    @Override
    public void completed(List employeeIds) {
        employeeIds.forEach((id) -> {
        // for each employee ID, get the name
        nameService.resolveTemplate("userId", id).request()
          .async()
          .get(new InvocationCallback() {
              @Override
              public void completed(String response) {
                     hashService.resolveTemplate("rawValue", response + id).request()
                    .async()
                    .get(new InvocationCallback() {
                        @Override
                        public void completed(String response) {
                            //complete the business logic
                        }
                        // ommitted implementation of the failed() method
                    });
              }
              // omitted implementation of the failed() method
          });
        });
    }
    // omitted implementation of the failed() method
});

// wait for inner requests to complete in 10 seconds
if (!completionTracker.await(10, TimeUnit.SECONDS)) {
    logger.warn("Some requests didn't complete within the timeout");
}

そのため、非同期で時間効率の良いコードを実現しましたが、次のようになります。

  • 読みにくい

  • 各呼び出しは新しいスレッドを生成します

すべての期待値がhash-service.によって配信されるのを待つために、すべてのコード例でCountDownLatchを使用していることに注意してください。これは、コードが単体テストで機能することを表明できるようにするためです。すべての期待値が実際に配信されたことを確認することによって。

通常のクライアントは待機しませんが、スレッドをブロックしないように、コールバック内で結果に対して行うべきことは何でもします。

3.3. 機能的で反応的なソリューション

機能的で事後対応的なアプローチは以下を提供します:

  • 優れたコードの読みやすさ

  • 流codingなコーディングスタイル

  • 効果的なスレッド管理

JAX-RSは、次のコンポーネントでこれらの目的をサポートしています。

  • CompletionStageRxInvoker は、デフォルトのリアクティブコンポーネントとしてCompletionStageインターフェースをサポートします

  • RxObservableInvokerProviderはRxJavaのObservable をサポートします

  • RxFlowableInvokerProviderはRxJavaのFlowableをサポートします

他のリアクティブライブラリのサポートを追加するためのAPIもあります。

4. JAX-RSリアクティブコンポーネントのサポート

4.1. JAX-RSのCompletionStage 

CompletionStage とその具体的な実装を使用して–CompletableFuture – weは、エレガントで非ブロッキングで流暢なサービスコールオーケストレーションを作成できます。

まず、ユーザーIDを取得します。

CompletionStage> userIdStage = userIdService.request()
  .accept(MediaType.APPLICATION_JSON)
  .rx()
  .get(new GenericType>() {
}).exceptionally((throwable) -> {
    logger.warn("An error has occurred");
    return null;
});

The rx() method call is the point from which the reactive handling kicks in.exceptionally関数を使用して、例外処理シナリオを流暢に定義します。

ここから、コールをきれいに調整してネームサービスからユーザー名を取得し、名前とユーザーIDの両方の組み合わせをハッシュできます。

List expectedHashValues = ...;
List receivedHashValues = new ArrayList<>();

// used to keep track of the progress of the subsequent calls
CountDownLatch completionTracker = new CountDownLatch(expectedHashValues.size());

userIdStage.thenAcceptAsync(employeeIds -> {
  logger.info("id-service result: {}", employeeIds);
  employeeIds.forEach((Long id) -> {
    CompletableFuture completable = nameService.resolveTemplate("userId", id).request()
      .rx()
      .get(String.class)
      .toCompletableFuture();

    completable.thenAccept((String userName) -> {
        logger.info("name-service result: {}", userName);
        hashService.resolveTemplate("rawValue", userName + id).request()
          .rx()
          .get(String.class)
          .toCompletableFuture()
          .thenAcceptAsync(hashValue -> {
              logger.info("hash-service result: {}", hashValue);
              receivedHashValues.add(hashValue);
              completionTracker.countDown();
          }).exceptionally((throwable) -> {
              logger.warn("Hash computation failed for {}", id);
              return null;
         });
    });
  });
});

if (!completionTracker.await(10, TimeUnit.SECONDS)) {
    logger.warn("Some requests didn't complete within the timeout");
}

assertThat(receivedHashValues).containsAll(expectedHashValues);

上記のサンプルでは、​​3つのサービスの実行を流で読み取り可能なコードで構成しています。

メソッドthenAcceptAsyncは、指定された関数after を実行します。指定されたCompletionStageは実行を完了しました(または例外をスローしました)。

連続する各呼び出しはブロックされず、システムリソースを適切に使用します。

マルチステップオーケストレーション(または単一のサービスコール)のThe CompletionStage interface provides a wide variety of staging and orchestration methods that allow us to compose, order and asynchronously execute any number of steps

4.2. JAX-RSのObservable 

Observable RxJavaコンポーネントを使用するには、最初にRxObservableInvokerProvider providerをクライアントに登録する必要があります(Jersey仕様書に記載されている「ObservableRxInvokerProvider” as」ではありません)。

Client client = client.register(RxObservableInvokerProvider.class);

次に、デフォルトの呼び出し側をオーバーライドします。

Observable> userIdObservable = userIdService
  .request()
  .rx(RxObservableInvoker.class)
  .get(new GenericType>(){});

この時点から、can use standard Observable semantics to orchestrate the processing flow

userIdObservable.subscribe((List listOfIds)-> {
  /** define processing flow for each ID */
});

4.3. JAX-RSのFlowable 

RxJavaのFlowable isを使用するためのセマンティクスは、Observable. のセマンティクスと同様です。適切なプロバイダーを登録します。

client.register(RxFlowableInvokerProvider.class);

次に、RxFlowableInvokerを指定します。

Flowable> userIdFlowable = userIdService
  .request()
  .rx(RxFlowableInvoker.class)
  .get(new GenericType>(){});

その後、通常のFlowable APIを使用できます。

5. 結論

JAX-RS仕様には、REST呼び出しのクリーンでノンブロッキングな実行を可能にする多数のオプションが用意されています。

特に、CompletionStage インターフェースは、さまざまなサービスオーケストレーションシナリオをカバーする堅牢な一連のメソッドと、スレッド管理をよりきめ細かく制御するためのカスタムExecutors を提供する機会を提供します。

この記事over on Githubのコードを確認できます。