API cliente JAX-RS réactive

API client JAX-RS réactive

1. introduction

Dans ce didacticiel, nous examinons la prise en charge par JAX-RS de la programmation réactive (Rx) à l’aide de l’API Jersey. Cet article suppose que le lecteur ait une connaissance de l'API client Jersey REST.

Une certaine familiarité avecreactive programming concepts sera utile mais n'est pas nécessaire.

2. Les dépendances

Premièrement, nous avons besoin des dépendances standard de la bibliothèque cliente Jersey:


    org.glassfish.jersey.core
    jersey-client
    2.27


    org.glassfish.jersey.inject
    jersey-hk2
    2.27

Ces dépendances nous fournissent un support de programmation réactif JAX-RS de base. Les versions actuelles dejersey-client etjersey-hk2 sont disponibles sur Maven Central.

Pour la prise en charge des frameworks réactifs tiers, nous utiliserons les extensions suivantes:


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

La dépendance ci-dessus fournit la prise en charge desObservable; de RxJava pour le plus récentFlowablede RxJava2, utilisez l'extension suivante:


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

Les dépendances versrxjava etrxjava2 sont également disponibles sur Maven Central.

3. Pourquoi nous avons besoin de clients JAX-RS réactifs

Supposons que nous ayons trois API REST à utiliser:

  • leid-service fournit une liste d'ID utilisateur longs

  • lename-service fournit un nom d'utilisateur pour un ID utilisateur donné

  • lehash-service renverra un hachage de l'ID utilisateur et du nom d'utilisateur

Nous créons un client pour chacun des services:

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}");

Ceci est un exemple artificiel, mais cela fonctionne dans le but de notre illustration. La spécification JAX-RS prend en charge au moins trois approches pour consommer ces services ensemble:

  • Synchrone (bloquant)

  • Asynchrone (non bloquant)

  • Réactif (fonctionnel, non bloquant)

3.1. Le problème de l'invocation de client Jersey synchrone

L'approche vanille de la consommation de ces services nous verra consommer lesid-service pour obtenir les ID utilisateur, puis appeler les APIname-service ethash-service séquentiellement pour chaque ID renvoyé.

With this approach, each call blocks the running thread until the request is fulfilled, passant beaucoup de temps au total à répondre à la demande combinée. Ceci est clairement moins que satisfaisant dans tout cas d'utilisation non trivial.

3.2. Le problème avec l'invocation asynchrone du client Jersey

Une approche plus sophistiquée consiste à utiliser le mécanismeInvocationCallback m pris en charge par JAX-RS. Dans sa forme la plus basique, nous transmettons un rappel à la méthodeget pour définir ce qui se passe lorsque l'appel d'API donné se termine.

Alors que nous obtenons maintenant une véritable exécution asynchrone (with some limitations on thread efficiency), il est facile de voir commentthis style of code can get unreadable and unwieldy dans tout sauf des scénarios triviaux. LeJAX-RS specification met spécifiquement en évidence ce scénario comme lePyramid 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");
}

Nous avons donc obtenu un code asynchrone et efficace dans le temps, mais:

  • c'est difficile à lire

  • chaque appel engendre un nouveau thread

Notez que nous utilisons unCountDownLatch dans tous les exemples de code afin d'attendre que toutes les valeurs attendues soient livrées par leshash-service. Nous faisons cela pour pouvoir affirmer que le code fonctionne dans un test unitaire en vérifiant que toutes les valeurs attendues ont bien été livrées.

Un client habituel n’attendrait pas, mais ferait tout ce qui devrait être fait avec le résultat dans le rappel pour ne pas bloquer le thread.

3.3. La solution fonctionnelle et réactive

Une approche fonctionnelle et réactive nous donnera:

  • Grande lisibilité du code

  • Style de codage courant

  • Gestion efficace des threads

JAX-RS prend en charge ces objectifs dans les composants suivants:

  • CompletionStageRxInvoker  prend en charge l'interfaceCompletionStage comme composant réactif par défaut

  • RxObservableInvokerProvider prend en charge lesObservable  de RxJava

  • RxFlowableInvokerProvider prend en charge lesFlowable de RxJava

Il existe également unAPI pour ajouter la prise en charge d'autres bibliothèques réactives.

4. Prise en charge des composants réactifs JAX-RS

4.1. CompletionStage in JAX-RS

En utilisant le sableCompletionStage , son implémentation concrète -CompletableFuture – we peut écrire une orchestration d'appel de service élégante, non bloquante et fluide.

Commençons par récupérer les ID utilisateur:

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. Nous utilisons la fonctionexceptionally pour définir couramment notre scénario de gestion des exceptions.

À partir de là, nous pouvons orchestrer proprement les appels pour extraire le nom d'utilisateur du service de noms, puis hacher la combinaison du nom et de l'ID utilisateur:

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);

Dans l'exemple ci-dessus, nous composons notre exécution des 3 services avec un code fluide et lisible.

La méthodethenAcceptAsync exécutera la fonction fournieafter lorsque l'exécution deCompletionStage étant terminée (ou levée une exception).

Chaque appel successif est non bloquant, faisant un usage judicieux des ressources système.

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 dans une orchestration en plusieurs étapes (ou un seul appel de service).

4.2. Observable in JAX-RS

Pour utiliser le composantObservable RxJava, nous devons d'abord enregistrer le sproviderRxObservableInvokerProvider (et non le «ObservableRxInvokerProvider” as est indiqué dans le document de spécification de Jersey) sur le client:

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

Ensuite, nous remplaçons l'invocateur par défaut:

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

A partir de là, oncan use standard Observable semantics to orchestrate the processing flow:

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

4.3. Flowable in JAX-RS

La sémantique d'utilisation de RxJavaFlowable  est similaire à celle deObservable.  Nous enregistrons le fournisseur approprié:

client.register(RxFlowableInvokerProvider.class);

Ensuite, nous fournissons lesRxFlowableInvoker:

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

Ensuite, nous pouvons utiliser la sAPIFlowable normale.

5. Conclusion

La spécification JAX-RS fournit un bon nombre d'options permettant une exécution propre et non bloquante des appels REST.

L'interfaceCompletionStage , en particulier, fournit un ensemble robuste de méthodes qui couvrent une variété de scénarios d'orchestration de services, ainsi que des opportunités de fournir desExecutors  personnalisés pour un contrôle plus fin de la gestion des threads.

Vous pouvez consulter le code de cet articleover on Github.