强制执行所需的 CompletableFuture 行为

Enforcing desired CompletableFuture behavior

我正在玩 CompletableFuture 链,偶然发现了一种具有意外行为的情况(至少对我而言):如果在 .thenCompose() 调用中传递了异常的 CompletableFuture,则生成的 CompletableFuture 将以原始方式完成包裹在 CompletionException 中的异常。不举例可能难以理解:

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

当然,我希望处理相同的 RuntimeException,无论链中之前或之后有多少转换。我有两个问题:

thenCompose() 的 JavaDoc 是:

Returns a new CompletionStage that, when this stage completes normally, is executed with this stage as the argument to the supplied function. See the CompletionStage documentation for rules covering exceptional completion.

以及接口状态的定义:

[…] In all other cases, if a stage's computation terminates abruptly with an (unchecked) exception or error, then all dependent stages requiring its completion complete exceptionally as well, with a CompletionException holding the exception as its cause. […]

作为 thenCompose returns 依赖阶段,这是预期行为

事实上,除了 CompletionException 之外的唯一情况是当您使用 completeExceptionally()cancel() 等方法显式完成 CompletableFuture 时等。甚至 supplyAsync() 等方法也会包装您的异常。

我认为没有任何其他选项可以访问原始异常,因为使用 getCause() 解包它已经很容易了。如果您真的需要经常这样做,您可以编写一个辅助方法,例如:

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

并按如下方式使用它:

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