La programmation asynchrone est au cœur de Vert.x, comme nous l’avons vu. Ici nous allons faire un petit retour sur des instructions déjà vues pour traiter des appels asynchrones et nous arrêter sur les types de base sur lesquels elles reposent.

L’interface Handler<E>

@FunctionalInterface
public interface Handler<E> {

  /**
   * Something has happened, so handle it.
   *
   * @param event  the event to handle
   */
  void handle(E event);
}
Listing 2‑9 Interface Handler

Cette interface sert à traiter le résultat d’un appel asynchrone, et doit donc être implémentée par l’appelant. Nous l’avons couramment utilisée dans les exemples rencontrés :

vertx.createHttpServer().requestHandler(request -> {
    // (...)
}).listen(8080, "localhost");

L’interface AsyncResult<T>

Représente le résultat d’un appel asynchrone, qui peut avoir échoué ou réussi. Ainsi un objet de cette interface sera souvent passé comme résultat de l’appel à une implémentation de l’interface Handler<E>. C’est aussi un type que nous avons déjà utilisé (consommation de l’AsyncResult de la ligne 3 à la ligne 10) :

 

HttpRequest<JsonObject> httpRequest = client.get(8081, "localhost", "/").as(BodyCodec.jsonObject());
vertx.createHttpServer().requestHandler(request -> {
    httpRequest.send(asyncResult -> {
        if (asyncResult.succeeded()) {
            String message = asyncResult.result().body().getString("message");
            request.response().end(message);
        } else {
            request.response().setStatusCode(500).end();
        }
    });
}).listen(8080, "localhost");
Listing 2‑10 Consommation d’un objet AsyncResult

L’interface Future<T>

Représente le résultat disponible ou à venir d’une action. Techniquement, un objet Future est un AsyncResult<T> et un Handler<AsyncResult<T>>.

Transmission de la valeur disponible de la Future

Comme résultat d’un appel asynchrone, une Future peut être utilisée comme suit, lorsque l’opération est un succès :

public void doSomethingAsync(Handler<AsyncResult<String>> handler) {
    // (...)
    handler.handle(Future.succeededFuture("Yeah!"));
    // (...)
}

Et lorsque l’opération est en erreur :

public void doSomethingAsync(Handler<AsyncResult<String>> handler) {
    // (...)
    Future.failedFuture(new IOException("Broken pipe"));
    // (...)
}

Consommation de la valeur à venir de la Future

En tant que résultat à venir d’une action, une Future peut être typiquement utilisée comme suit par le service :

Future<String> future = Future.future();
 // (...)
// beaucoup plus tard
future.complete("Value");

Vu que la valeur de la Future n’est pas immédiatement disponible, le consommateur doit enregistrer sur la Future un Handler<AsyncResult<T>> qui sera appelée lorsque la valeur de la Future sera disponible :

future.setHandler(event -> System.out.println(event.result()));

Si au contraire l’action se termine en erreur, la Future peut être mise en erreur :

Future<String> future = Future.future();
// (...)
// beaucoup plus tard
future.fail("Fail!");

De la même façon, si un handler est enregistré sur la Future, il sera notifié de la disponibilité du résultat de la Future et du statut d’erreur.

Composition des Futures

Les Futures présentent de nombreux opérateurs qui permettent de les composer, transformer, doter de valeurs par défaut lorsqu’elles finissent en erreur, etc. L’usage de certains de ces opérateurs est illustré dans les exemples de code suivants.

Future<String> f1 = Future.future();
Future<Integer> f2 = f1.compose(str -> Future.succeededFuture(str.length()))
        .map(n -> n * 2)
        .otherwise(0)
        .setHandler(ar -> {
            if (ar.succeeded()) {
                System.out.println(ar.result());
            } else {
                ar.cause().printStackTrace();
           }
       });
Listing 2‑11 Opérations sur les Futures

Commentons le code ci-dessus :

  • Ligne 1 : Création de la Future f1
  • Ligne 2 : composition de la Future f1, c’est-à-dire création d’une Future avec la valeur (actuelle ou à venir) de f1.
  • Ligne 3 : transformation de la valeur de la précédente Future par l’opération map qui retourne une Future avec la valeur de l’opération.
  • Ligne 4 : l’opérateur otherwise permet de retourner une Future de substitution lorsque la Future sur laquelle est appelée l’opérateur finit en erreur.
  • Ligne 5 : nous avons déjà vu la méthode setHandler qui permet d’enregistrer le handler qui va traiter la valeur ou l’erreur de la Future.

Si nous terminons la Future f1 comme suite :

f1.complete("abc");

Le handler affichera à la suite de l’application des opérateurs du listing 2.11 la valeur 6.

Si au contraire la Future f1 termine en erreur :

f1.fail(new IllegalStateException("Just won't do"));

Le handler du listing 2.11 affichera quand même la valeur 0, fournie par la Future de fallback à la ligne 4.

Les opérateurs que nous venons de voir sont séquentiels, c’est-à-dire que les Futures sont produites au fur et à mesure de l’exécution des opérateurs. Mais il existe des situations où les exécutions concurrentes d’un ensemble de Futures doivent décider de la prochaine instruction à exécuter, Vert.x fournit trois opérations pour gérer ces situations.

CompositeFuture.all

Cette méthode accepte en arguments jusqu’à 6 Futures (au-delà on peut passer un objet liste de Futures) et retourne une Future qui aura le statut de succès si toutes les Futures en argument sont en succès, sinon aura le statut d’erreur si au moins une des Futures en argument est en erreur. La complétion de la Future résultat est faite à la fin de la complétion de toutes les Futures si elles finissent en succès, sinon elle est faite à la complétion de la première Future qui finira en erreur.

CompositeFuture.join

Comme l’opérateur CompositeFuture.all, accepte en arguments jusqu’à 6 Futures (au-delà on peut passer un objet liste de Futures) et retourne une Future qui aura le statut de succès si toutes les Futures en argument sont en succès, sinon aura le statut d’erreur si au moins une des Futures en argument est en erreur. Mais à la différence de l’opérateur CompositeFuture.all, la complétion de la Future résultat est faite à la complétion de toutes les Futures en argument, quels que soient leurs statuts de complétion.

CompositeFuture.any

Accepte en arguments jusqu’à 6 Futures (au-delà on peut passer un objet liste de Futures) et retourne une Future qui aura le statut de succès si au moins une des Futures en argument se termine en succès, sinon aura le statut d’erreur si toutes les Futures en argument se terminent en erreur. La complétion de la Future résultat est faite lors de la complétion de la première Future en succès, et s’il n’y en a pas (toutes les Futures en erreur), elle se fera à la dernière complétion de toutes les Futures en arguments.