2016-10-31 2 views
1

Je joue avec les chaînes CompletableFuture et suis tombé sur une situation avec un comportement inattendu (pour moi, au moins): si un CompletableFuture exceptionnel est passé en appel .thenCompose(), le CompletableFuture résultant sera terminé avec l'exception originale enveloppée au CompletionException. Il peut être difficile de comprendre sans exemple:Exécution du comportement CompletableFuture souhaité

public static <T> CompletableFuture<T> exceptional(Throwable error) { 
    CompletableFuture<T> future = new CompletableFuture<>(); 
    future.completeExceptionally(error); 
    return future; 
} 

public static void main(String[] args) { 
    CompletableFuture<Void> exceptional = exceptional(new RuntimeException()); 
    exceptional 
      .handle((result, throwable) -> { 
       System.out.println(throwable); 
       // java.lang.RuntimeException 

       System.out.println(throwable.getCause()); 
       // null 

       return null; 
      }); 

    CompletableFuture 
      .completedFuture(null) 
      .thenCompose(v -> exceptional) 
      .handle((result, throwable) -> { 
       System.out.println(throwable); 
       // java.util.concurrent.CompletionException: java.lang.RuntimeException 

       System.out.println(throwable.getCause()); 
       // java.lang.RuntimeException 

       return null; 
      }); 
} 

Bien sûr que je comptais faire face à la même RuntimeException peu importe combien de transformations étaient avant ou après dans la chaîne. J'ai deux questions:

  • Est-ce un comportement attendu?
  • Ai-je des options pour conserver l'exception d'origine, sauf pour le déballage manuel?

Répondre

2

Le JavaDoc pour thenCompose() est:

Renvoie une nouvelle CompletionStage que, lorsque cette étape se termine normalement, est exécuté avec ce stade que l'argument de la fonction fournie. Voir la documentation CompletionStage pour les règles couvrant l'achèvement exceptionnel.

et la définition des états d'interface:

[...] Dans tous les autres cas, si le calcul d'une étape se termine brusquement avec une (non cochée) exception ou erreur, toutes les étapes dépendantes nécessitant son achèvement compléter exceptionnellement aussi, avec un CompletionException en tenant l'exception comme sa cause. [...]

Comme thenCompose renvoie un stade à charge, ce comportement est normal.

En fait, les seuls cas où vous pourriez avoir quelque chose d'autre qu'un CompletionException est lorsque vous remplissez un CompletableFuture explicitement avec des méthodes comme completeExceptionally(), cancel() etc. Même des méthodes telles que supplyAsync() enveloppera vos exceptions.

Je ne pense pas qu'il existe une autre option pour accéder à l'exception d'origine car il est déjà assez facile de le déplier avec getCause(). Si vous avez vraiment besoin de le faire souvent, vous pourriez écrire une méthode d'assistance tels que:

public static <T, U> BiFunction<? super T, Throwable, ? extends U> 
     unwrappingCompletionException(BiFunction<? super T, Throwable, ? extends U> fn) { 
    return (t, u) -> { 
     if (u instanceof CompletionException) { 
      return fn.apply(t, u.getCause()); 
     } 
     return fn.apply(t, u); 
    }; 
} 

et l'utiliser comme suit:

CompletableFuture 
     .completedFuture(null) 
     .thenCompose(v -> exceptional) 
     .handle(unwrappingCompletionException((result, throwable) -> { 
      […] 
     }));