web-dev-qa-db-fra.com

CompletableFuture avale les exceptions?

J'ai joué avec CompletableFuture et j'ai remarqué une chose étrange.

String url = "http://google.com";

CompletableFuture<String> contentsCF = readPageCF(url);
CompletableFuture<List<String>> linksCF = contentsCF.thenApply(_4_CompletableFutures::getLinks);

linksCF.thenAccept(list -> {
    assertThat(list, not(empty()));
});

linksCF.get();

Si, dans mon appel thenAccept, l'assertion échoue, l'exception n'est pas propagée. J'ai alors essayé quelque chose de plus laid encore:

linksCF.thenAccept(list -> {
    String a = null;
    System.out.println(a.toString());
});

rien ne se passe, aucune exception n'est propagée. J'ai essayé d'utiliser des méthodes comme handle et d'autres liées aux exceptions dans CompletableFutures, mais j'ai échoué - aucune ne propage l'exception comme prévu.

Lorsque j'ai débogué le CompletableFuture, il intercepte l'exception comme ceci:

final void internalComplete(T v, Throwable ex) {
    if (result == null)
        UNSAFE.compareAndSwapObject
            (this, RESULT, null,
             (ex == null) ? (v == null) ? NIL : v :
             new AltResult((ex instanceof CompletionException) ? ex :
                           new CompletionException(ex)));
    postComplete(); // help out even if not triggered
}

et rien d'autre.

Je suis sur JDK 1.8.0_05 x64, Windows 7.

Est-ce que j'ai râté quelque chose?

40
maciej

Le problème est que vous ne demandez jamais à recevoir les résultats de votre appel à linksCF.thenAccept(..).

Votre appel à linksCF.get() attendra les résultats de l'exécution dans votre chaîne. Mais il ne renverra que les résultats du futur linksCF. Cela n'inclut pas les résultats de votre assertion.

linksCF.thenAccept(..) renverra une nouvelle instance de CompletableFuture. Pour obtenir l'exception levée, appelez get() ou vérifiez l'état de l'exception avec isCompletedExceptionally() sur la nouvelle instance CompletableFuture de retour.

CompletableFuture<Void> acceptedCF = linksCF.thenAccept(list -> {
    assertThat(list, not(empty()));
});

acceptedCF.exceptionally(th -> {
    // will be executed when there is an exception.
    System.out.println(th);
    return null;
});
acceptedCF.get(); // will throw ExecutionException once results are available

Alternative?

CompletableFuture<List<String>> appliedCF = linksCF.thenApply(list -> {
    assertThat(list, not(empty()));
    return list;
});

appliedCF.exceptionally(th -> {
    // will be executed when there is an exception.
    System.out.println(th);
    return Coolections.emptyList();
});
appliedCF.get(); // will throw ExecutionException once results are available
24
Gregor Koukkoullis

Bien que Gregor Koukkoullis (+1) ait déjà répondu à la question, voici un MCVE que j'ai créé pour tester cela.

Il existe plusieurs options pour obtenir l'exception réelle à l'origine du problème en interne. Cependant, je ne vois pas pourquoi appeler get sur le futur retourné par thenAccept devrait être un problème. Dans le doute, vous pouvez également utiliser thenApply avec la fonction d'identité et utiliser un joli modèle fluide, comme dans

List<String> list = 
    readPage().
    thenApply(CompletableFutureTest::getLinks).
    thenApply(t -> {
        // check assertion here
        return t;
    }).get();

Mais il y a peut-être une raison particulière pour laquelle vous voulez éviter cela.

import Java.util.ArrayList;
import Java.util.List;
import Java.util.concurrent.CompletableFuture;
import Java.util.concurrent.ExecutionException;
import Java.util.function.Supplier;

public class CompletableFutureTest
{
    public static void main(String[] args) 
        throws InterruptedException, ExecutionException
    {
        CompletableFuture<String> contentsCF = readPage();
        CompletableFuture<List<String>> linksCF = 
            contentsCF.thenApply(CompletableFutureTest::getLinks);

        CompletableFuture<Void> completionStage = linksCF.thenAccept(list -> 
        {
            String a = null;
            System.out.println(a.toString());
        });        

        // This will NOT cause an exception to be thrown, because
        // the part that was passed to "thenAccept" will NOT be
        // evaluated (it will be executed, but the exception will
        // not show up)
        List<String> result = linksCF.get();
        System.out.println("Got "+result);


        // This will cause the exception to be thrown and
        // wrapped into an ExecutionException. The cause
        // of this ExecutionException can be obtained:
        try
        {
            completionStage.get();
        }
        catch (ExecutionException e)
        {
            System.out.println("Caught "+e);
            Throwable cause = e.getCause();
            System.out.println("cause: "+cause);
        }

        // Alternatively, the exception may be handled by
        // the future directly:
        completionStage.exceptionally(e -> 
        { 
            System.out.println("Future exceptionally finished: "+e);
            return null; 
        });

        try
        {
            completionStage.get();
        }
        catch (Throwable t)
        {
            System.out.println("Already handled by the future "+t);
        }

    }

    private static List<String> getLinks(String s)
    {
        System.out.println("Getting links...");
        List<String> links = new ArrayList<String>();
        for (int i=0; i<10; i++)
        {
            links.add("link"+i);
        }
        dummySleep(1000);
        return links;
    }

    private static CompletableFuture<String> readPage()
    {
        return CompletableFuture.supplyAsync(new Supplier<String>() 
        {
            @Override
            public String get() 
            {
                System.out.println("Getting page...");
                dummySleep(1000);
                return "page";
            }
        });
    }

    private static void dummySleep(int ms)
    {
        try
        {
            Thread.sleep(ms);
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}
6
Marco13

Si, dans mon appel thenAccept, l'assertion échoue, l'exception n'est pas propagée.

La suite que vous enregistrez avec thenAccept() est une tâche distincte de l'avenir linksCF. La tâche linksCF s'est terminée avec succès; il n'y a aucune erreur à signaler. Il a sa valeur finale. Une exception levée par linksCF ne devrait indiquer qu'un problème produisant le résultat de linksCF; si un autre morceau de code qui consomme le résultat jette, cela n'indique pas une incapacité à produire le résultat.

Pour observer une exception qui se produit dans une continuation, vous devez observer le CompletableFuture de la continuation.

correct. mais 1) je ne devrais pas être forcé d'appeler get () - l'un des points des nouvelles constructions; 2) il est enveloppé dans une ExecutionException

Et si vous vouliez transmettre le résultat à plusieurs suites indépendantes à l'aide de thenAccept()? Si une de ces continuations devait être lancée, pourquoi cela aurait-il un impact sur le parent ou les autres continuations?

Si vous voulez traiter linksCF comme un noeud dans une chaîne et observer le résultat (et toutes les exceptions) qui se produisent dans la chaîne, alors vous devez appeler get() sur le dernier maillon de la chaîne .

Vous pouvez éviter le ExecutionException vérifié en utilisant join() au lieu de get(), ce qui encapsulera l'erreur dans un CompletionException non vérifié (mais il est toujours encapsulé ).

2
Mike Strobel

Les réponses ici m'ont aidé à gérer les exceptions dans CompletableFuture, en utilisant la méthode "exceptionnaly", mais il manquait un exemple de base, alors en voici un, inspiré de la réponse de Marco13:

/**
 * Make a future launch an exception in the accept.
 *
 * This will simulate:
 *  - a readPage service called asynchronously that return a String after 1 second
 *  - a call to that service that uses the result then throw (eventually) an exception, to be processed by the exceptionnaly method.
 *
 */
public class CompletableFutureTest2
{
    public static void main(String[] args)
        throws InterruptedException, ExecutionException
    {
        CompletableFuture<String> future = readPage();

        CompletableFuture<Void> future2 = future.thenAccept(page->{
            System.out.println(page);
            throw new IllegalArgumentException("unexpected exception");
        });

        future2.exceptionally(e->{
          e.printStackTrace(System.err);
          return null;
        });

    }

    private static CompletableFuture<String> readPage()
    {

      CompletableFuture<String> future = new CompletableFuture<>();
      new Thread(()->{
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
        }

        // FUTURE: normal process
        future.complete("page");

      }).start();
        return future;
    }


}

L'erreur à éviter est d'appeler "exceptionnellement" sur le 1er futur (le futur variable dans mon code) au lieu du futur retourné par le "thenAccept" qui contient le lambda qui peut lever une exception (la variable future2 dans mon code). .

2
pdem

Comme d'habitude, la compréhension du comportement de CompletableFuture est mieux laissée aux documents officiels et à un blog.

Chaque méthode de chaînage then...() de la classe CompletableFuture, qui implémente CompletionStage , accepte un argument a CompletionStage. L'étape passée dépend de l'ordre des méthodes then...() que vous avez chaînées. Encore une fois, docs, mais voici ce blog susmentionné .

0
jeffery