web-dev-qa-db-ja.com

CompletableFutureは例外を飲み込みますか?

私はCompletableFutureをいじってみましたが、奇妙なことに気づきました。

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

thenAccept呼び出しでアサーションが失敗した場合、例外は伝播されません。私はそれからもっとsomethingい何かを試しました:

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

何も起こらず、例外は伝播されません。 handleなどの例外に関連するCompletableFuturesなどのメソッドを使用しようとしましたが、失敗しました-期待どおりに例外が伝播していません。

CompletableFutureをデバッグすると、次のように例外をキャッチします。

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
}

何もありません。

私はJDK 1.8.0_05 x64、Windows 7を使用しています。

ここに何かが足りませんか?

40
maciej

問題は、linksCF.thenAccept(..)への呼び出しの結果の受信を決して要求しないことです。

linksCF.get()への呼び出しは、チェーン内の実行結果を待ちます。ただし、その後、CFの結果を返すだけです。これには、アサーションの結果は含まれません。

linksCF.thenAccept(..)は、新しいCompletableFutureインスタンスを返します。スローされた例外を取得するには、get()を呼び出すか、新しく返されるCompletableFutureインスタンスでisCompletedExceptionally()を使用して例外ステータスを確認します。

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

代替案?

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(+1)によって既に回答されていますが、ここに [〜#〜] mcve [〜#〜] があり、これをテストするために作成しました。

内部的に問題を引き起こした実際の例外を取得するには、いくつかのオプションがあります。ただし、getによって返されるfutureでthenAcceptを呼び出すことが問題になる理由はわかりません。疑わしいのは、thenApplyを恒等関数で使用し、次のようなNice fluentパターンを使用することもできます。

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

しかし、おそらくこれを避けたい特定の理由があります。

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

ThenAccept呼び出しでアサーションが失敗した場合、例外は伝達されません。

thenAccept()で登録する継続は、linksCF futureとは別のタスクです。 linksCFタスクは正常に完了しました。報告するエラーはありません。最終的な価値があります。 linksCFによってスローされる例外は、linksCFの結果を生成する問題のみを示す必要があります。結果を消費する他のコードがスローされる場合、結果の生成に失敗したことを示していません。

継続で発生する例外を監視するには、継続のCompletableFutureを監視する必要があります。

正しい。しかし、1)get()の呼び出しを強制されるべきではありません-新しい構成要素のポイントの1つ。 2)ExecutionExceptionにラップされている

thenAccept()を使用して、複数の独立した継続に結果を引き渡したい場合はどうなりますか?これらの継続の1つがスローされた場合、なぜそれが親または他の継続に影響を与えるのでしょうか?

linksCFをチェーン内のノードとして扱い、チェーン内で発生する結果(および例外)を観察する場合は、チェーン内の最後のリンクでget()を呼び出す必要があります。

join()の代わりにget()を使用すると、チェックされていないExecutionExceptionを回避できます。これにより、エラーは未チェックのCompletionExceptionにラップされます(ただし、ラップされます)。

2
Mike Strobel

ここでの回答は"exceptionnaly"メソッドを使用してCompletableFutureの例外を管理するのに役立ちましたが、基本的な例を逃したため、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;
    }


}

避けるべき間違いは、例外をスローする可能性のあるラムダ(コード内の変数future2)を含む "thenAccept"によって返される未来の代わりに、1番目の未来(私のコード内の変数future)で "exceptionnaly"を呼び出すことです。 。

2
pdem

いつものように、CompletableFutureの動作を理解することは、公式ドキュメントとブログに任せたほうがよいでしょう。

CompletionStage を実装するCompletableFutureクラスの各then...()チェーンメソッドは、引数CompletionStageを受け入れます。渡されるステージは、チェーンしたthen...()メソッドの順序によって異なります。繰り返しになりますが、これは 前述のブログ です。

0
jeffery