web-dev-qa-db-fra.com

Un gestionnaire d'exceptions passé à CompletableFuture.exceptionally () doit-il renvoyer une valeur significative?

Je suis habitué au modèle ListenableFuture , avec les rappels onSuccess() et onFailure(), par exemple.

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFuture<String> future = service.submit(...)
Futures.addCallback(future, new FutureCallback<String>() {
  public void onSuccess(String result) {
    handleResult(result);
  }
  public void onFailure(Throwable t) {
    log.error("Unexpected error", t);
  }
})

Il semble que Java 8's CompletableFuture est destiné à gérer plus ou moins le même cas d'utilisation. Naïvement, je pourrais commencer à traduire l'exemple ci-dessus comme :

CompletableFuture<String> future = CompletableFuture<String>.supplyAsync(...)
  .thenAccept(this::handleResult)
  .exceptionally((t) -> log.error("Unexpected error", t));

C'est certainement moins verbeux que la version ListenableFuture et semble très prometteur.

Cependant, il ne compile pas, car exceptionally() ne prend pas de Consumer<Throwable>, Il prend un Function<Throwable, ? extends T> - dans ce cas, un Function<Throwable, ? extends String> .

Cela signifie que je ne peux pas simplement enregistrer l'erreur, je dois trouver une valeur String à retourner dans le cas d'erreur, et il n'y a pas de valeur String significative à renvoyer dans l'erreur Cas. Je peux retourner null, juste pour obtenir le code à compiler:

  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return null; // hope this is ignored
  });

Mais cela commence à devenir verbeux à nouveau, et au-delà de la verbosité, je n'aime pas avoir ce null flottant - cela suggère que quelqu'un pourrait essayer de récupérer ou de capturer cette valeur, et cela à un moment donné beaucoup plus tard Je pourrais avoir un NullPointerException inattendu.

Si exceptionally() prenait un Function<Throwable, Supplier<T>> Je pourrais au moins faire quelque chose comme ça -

  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return () -> { 
      throw new IllegalStateException("why are you invoking this?");
    }
  });

- mais ce n'est pas le cas.

Quelle est la bonne chose à faire lorsque exceptionally() ne doit jamais produire une valeur valide? Y a-t-il autre chose que je peux faire avec CompletableFuture, ou autre chose dans les nouvelles bibliothèques Java 8, qui prend mieux en charge ce cas d'utilisation?

17
David Moles

Une transformation correspondante correcte avec CompletableFuture est:

CompletableFuture<String> future = CompletableFuture.supplyAsync(...);
future.thenAccept(this::handleResult);
future.exceptionally(t -> {
    log.error("Unexpected error", t);
    return null;
});

Autrement:

CompletableFuture<String> future = CompletableFuture.supplyAsync(...);
future
    .whenComplete((r, t) -> {
        if (t != null) {
            log.error("Unexpected error", t);
        }
        else {
            this.handleResult(r);
        }
    });

La partie intéressante ici est que vous enchaîniez les futurs dans vos exemples. La syntaxe apparemment fluide enchaîne en fait les futurs, mais il semble que vous ne vouliez pas cela ici.

L'avenir renvoyé par whenComplete peut être intéressant si vous souhaitez renvoyer un avenir qui traite quelque chose avec le résultat d'un avenir interne. Il préserve l'exception de l'avenir actuel, le cas échéant. Cependant, si l'avenir se termine normalement et que la suite se lance, il se terminera exceptionnellement avec l'exception levée.

La différence est que tout ce qui se passe après la fin de future se produira avant la prochaine continuation. L'utilisation de exceptionally et thenAccept est équivalente si vous êtes l'utilisateur final de future, mais si vous fournissez un futur à un appelant, l'un ou l'autre sera traité sans notification d'achèvement (comme si vous étiez en arrière-plan, si vous le pouvez), très probablement la continuation exceptionally puisque vous voudrez probablement que l'exception se répercute en cascade sur les autres continuations.

12
acelent

Notez que exceptionally(Function<Throwable,? extends T> fn) renvoie également CompletableFuture<T>. Vous pouvez donc enchaîner plus.

La valeur de retour de Function<Throwable,? extends T> est destiné à produire un résultat de secours pour les méthodes enchaînées suivantes. Ainsi, vous pouvez par exemple obtenir la valeur du cache si elle n'est pas disponible dans la base de données.

CompletableFuture<String> future = CompletableFuture<String>.supplyAsync(/*get from DB*/)
  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return "Fallback value from cache";
  })
  .thenAccept(this::handleResult);

Si exceptionally accepterait Supplier<T> au lieu de fonction, alors comment cela pourrait renvoyer un CompletableFuture<String> pour enchaîner plus loin?

Je pense que vous voulez une variante de exceptionally qui retournerait void. Mais malheureusement, non, il n'y a pas une telle variante.

Donc, dans votre cas, vous pouvez retourner en toute sécurité n'importe quelle valeur de cette fonction de secours, si vous ne retournez pas cet objet future et ne l'utilisez pas plus loin dans votre code (il ne peut donc pas être chaîné plus loin). Mieux vaut ne pas l'affecter à une variable.

CompletableFuture<String>.supplyAsync(/*get from DB*/)
  .thenAccept(this::handleResult)
  .exceptionally((t) -> {
    log.error("Unexpected error", t);
    return null;
  });
2