【问题标题】:How to Obtain the Exception Outside of CompletableFuture Handler?如何获取 CompletableFuture 处理程序之外的异常?
【发布时间】:2022-03-01 02:02:36
【问题描述】:

我有以下情况,我正在尝试查看是否有解决方案:

  • 必须并行进行两个 Spring 服务调用(一个是现有的服务调用/逻辑,第二个是新添加的)。
  • 结果应该被合并并由 RESTful API 返回。

一条快乐的道路应该是直截了当的,但是,当涉及到服务发出的错误时,应该遵守以下规则:

  • API 仅在两个服务调用都失败时才会失败——这应该从主线程而不是 @Async 池中抛出,因为它们是独立线程并且无法访问彼此的异常(至少这是我的推理)。

  • 如果只有一个失败,则通过另一个服务(异步)记录错误,API 仅返回成功服务的结果——这可以通过各自的 @Async 线程完成。

    @Service
    public class Serv1 interface ServInf {
     @Async("customPool")
     public CompletableFuture<List<Obj>> getSomething(int id) {
       // The service ensures that the list is never null, but it can be empty
       return CompletableFuture.completedFuture(/* calling an external RESTful API */);
     }
    }
    
    @Service
    public class Serv2 interface ServInf {
     @Async("customPool")
     public CompletableFuture<List<Obj>> getSomething(int id) {
       // The service ensures that the list is never null, but it can be empty
       return CompletableFuture.completedFuture(/* calling another external RESTful API */);
         }
     }
    
    @RestController
    public class MyController {
    
     /** Typical service @Autowired's */
    
     @GetMapping(/* ... */)
     public WrapperObj getById(String id) {
    
         CompletableFuture<List<String>> service1Result =
                 service1.getSomething(id)
                         .thenApply(result -> {
                             if (result == null) { return null; }
                             return result.stream().map(Obj::getName).collect(Collectors.toList());
                         })
                         .handle((result, exception) -> {
                             if (exception != null) {
                                 // Call another asynchronous logging service which should be easy
                                 return null;
                             } else {
                                 return result;
                             }
                         });
    
         CompletableFuture<List<String>> service2Result =
                 service2.getSomething(id)
                         .thenApply(result -> {
                             if (result == null) { return null; }
                             return result.stream().map(Obj::getName).collect(Collectors.toList());
                         })
                         .handle((result, exception) -> {
                             if (exception != null) {
                                 // Call another asynchronous logging service which should be easy
                                 return null;
                             } else {
                                 return result;
                             }
                         });
    
         // Blocking till we get the results from both services
         List<String> result1 = service1Result.get();
         List<String> result2 = service2Result.get();
    
         /** Where to get the exceptions thrown by the services if both fail
         if (result1 == null && result2 == null) {
             /** Signal that the API needs to fail as a whole */
             throw new CustomException( /** where to get the messages? */);
         }
    
         /** merge and return the result */
     }
    }
    

我的问题是,由于这些服务返回一些对象的列表,即使我使用CompletableFuture.handle() 并检查是否存在异常,我也无法返回异常本身以捕获并让 Spring Advice 类处理它(链接以返回一个列表)。

我想到的一件事是使用AtomicReference 来捕获异常并将它们设置在handle() 中,并在期货完成/完成后使用它们,例如

AtomicReference<Throwable> ce1 = new AtomicReference<>();
AtomicReference<Throwable> ce2 = new AtomicReference<>();

.handle((result, exception) -> {
    if (exception != null) {
        ce1.set(exception);
        return null; // This signals that there was a failure
    } else {
        return result;
    }
});

List<String> result1 = service1Result.get();
List<String> result2 = service2Result.get();

/** Where to get the exceptions thrown by the services if both fail
if (result1 == null && result2 == null) {
    /** Signal that the API needs to fail as a whole */
    throw new CustomException(/** do logic to capture ce1.get().getMessage() + ce2.get().getMessage() */);
}

首先,这听起来像是多线程异步调用中的可行解决方案吗?

其次,这看起来很乱,所以我想知道是否有更优雅的方法可以在 Spring 异步池之外捕获这些异常,并在主线程中处理它,例如将异常信息组合起来,扔给 Spring Advice 异常处理程序。

【问题讨论】:

  • 既然你在 Spring 生态系统中,你有没有研究过 Reactor/webflux?
  • .get() 将抛出异常(如果有),因此您可以在 .get()s 周围使用一个很好的旧 try/catch 并同步处理这些异常。
  • @ThomasTimbul,两件事:1)旧的服务调用必须留在RestTemplate,因为外部服务调用将在明年半内退休(我们不碰它),2)对外部 API 的第二次服务调用将在 Reactor WebClient 中进行调用,但是,在收到来自 WebClient 的结果后需要执行一些逻辑——这就是为什么我认为我有在单独的@Async 线程中为新服务处理这些逻辑(如果这不正确,请告知)。
  • @sp00m 这是否也会捕获ServInf.doSomething() 抛出的异常? get() 似乎只抛出了一些异常。
  • 请进一步澄清:WrapperObj 的定义是什么?如果幸福的道路只涉及其中一个结果,那么您为什么要让这些服务相互竞争?两者都不是更可取:负载平衡(智能?);总是喜欢一个,只有在失败时才调用另一个(最容易实现?); ……?关于我之前的评论,你可以在额外的 Reactor 转换中执行额外的逻辑(事实上你应该,以保持一切反应并防止整个事情在你用完线程时卡住)。

标签: java exception completable-future


【解决方案1】:

假设两个期货

CompletableFuture<List<String>> service1Result = …
CompletableFuture<List<String>> service2Result = …

将这两种期货结合起来的直接方法是

CompletableFuture<List<String>> both = service1Result.thenCombine(service2Result,
    (list1, list2) -> Stream.concat(list1.stream(), list2.stream())
                            .collect(Collectors.toList()));

但是,如果任何一个未来失败,这个未来都会失败。

要仅在两个future 都失败时失败并从两个throwable 构造一个新异常,我们可以定义两个实用方法:

private static Throwable getThrowable(CompletableFuture<?> f) {
    return f.<Throwable>thenApply(value -> null)
            .exceptionally(throwable -> throwable).join();
}

private static <T> T throwCustom(Throwable t1, Throwable t2) {
    throw new CustomException(t1.getMessage() + " and " + t2.getMessage());
}

getThrowable 方法旨在与已知的未来异常完成一起使用。我们可以调用join 并捕获异常,但如上所示,我们还可以将转换未来转换为包含可抛出对象作为其值的非异常未来。

然后,我们可以将以上所有内容结合起来

CompletableFuture<List<String>> failOnlyWhenBothFailed = both
    .thenApply(list -> both)
    .exceptionally(t ->
        !service1Result.isCompletedExceptionally()? service1Result:
        !service2Result.isCompletedExceptionally()? service2Result:
        throwCustom(getThrowable(service1Result), getThrowable(service2Result)))
    .thenCompose(Function.identity());

在传递给exceptionally 的函数中,已知传入的futures 已经完成,因此我们可以使用实用方法来提取throwables 并抛出一个新的异常。

这样做的好处是生成的构造是非阻塞的。

但在您的情况下,您希望等待完成而不是返回未来,因此我们可以简化操作:

CompletableFuture<List<String>> both = service1Result.thenCombine(service2Result,
    (list1, list2) -> Stream.concat(list1.stream(), list2.stream())
                            .collect(Collectors.toList()));

both.exceptionally(t -> null).join();

if(service1Result.isCompletedExceptionally()&&service2Result.isCompletedExceptionally()){
  Throwable t1 = getThrowable(service1Result), t2 = getThrowable(service2Result);
  throw new CustomException(t1.getMessage() + " and " + t2.getMessage());
}

List<String> result = (
    service1Result.isCompletedExceptionally()? service2Result:
    service2Result.isCompletedExceptionally()? service1Result: both
).join();

通过使用both.exceptionally(t -&gt; null).join();,我们等待两个作业的完成,而不会在失败时抛出异常。在此语句之后,我们可以安全地使用isCompletedExceptionally() 来检查我们知道要完成的期货。

因此,如果两者都失败,我们提取可抛出对象并抛出我们的自定义异常,否则,我们检查哪些任务成功并提取其中一个或两者的结果。

【讨论】:

  • 直到这个答案我才注意到,但是exceptionally 不允许更改初始CompletableFuture 的类型,这就是为什么你求助于一个非常有趣的:f.&lt;Throwable&gt;thenApply(value -&gt; null)。从这个广泛的答案中可能并不明显,但我真的很喜欢它。
  • @Holger 谢谢。我的一个问题是,由于这些服务是使用 spring @Async 执行并返回 CompletableFutureCompletableFuture.completedFuture(),那么在 List&lt;String&gt; result 上最后一个 join() 的目的是什么?附言使用这种方法,我可以使用isCompletedExceptionally() 在只有单个服务失败时记录并返回其他成功服务的结果。答案:哎呀!获得实际的基础结果。抱歉错过了。
【解决方案2】:

仅仅因为我提出了它作为一种可能性,我想这样的事情应该在使用 Project Reactor 的世界中工作:

首先我们修改服务以返回Monos,这很容易使用Mono.fromFuture(或者您可以将一个服务转换为 Reactor 样式,如果并且一旦它准备好):

@Service
public class Serv1 implements ServInf {
    public Mono<List<Obj>> getSomething(int id) {
        // The service ensures that the list is never null, but it can be empty
        return Mono.fromFuture(CompletableFuture.completedFuture(/* calling an external RESTful API */));
        //This Mono will either emit the result or complete with an error in case of Exception
    }
}

//similar for Serv2

(反应式)端点可能如下所示(请参阅下面编号的 cmets):

public Mono<WrapperObj> getById(String id) {
        WrapperObj wrapper = new WrapperObj(); //1
        Mono<Optional<List<Obj>>> s1Mono = serv1.getSomething(id)
            .subscribeOn(Schedulers.boundedElastic()) //2
            .map(Optional::ofNullable) //3
            .doOnError(wrapper::setS1ErrorResult) //4
            .onErrorResume(t -> Mono.just(Optional.empty())); //5

        Mono<Optional<List<Obj>>> s2Mono = serv2.getSomething(id)
            .subscribeOn(Schedulers.boundedElastic()) //2
            .map(Optional::ofNullable) //3
            .doOnError(wrapper::setS2ErrorResult) //4
            .onErrorResume(t -> Mono.just(Optional.empty())); //5

        return s1Mono
            .zipWith(s2Mono) //6
            .map(result ->
                //transforms non-error results and merges them into the wrapper object
                transformResult(result.getT1().orElse(null), result.getT2().orElse(null), wrapper) //7
            )
            .switchIfEmpty(Mono.just(wrapper)) //8

        ;
    }

评论:

  1. 结果用于“累积”结果和异常

  2. boundedElastic线程池上调用服务,推荐用于较长的IO任务。

  3. 将结果包装在Optional 中。我使用空的 Optional 作为错误完成的方便结果,因为 nulls 不能很好地通过 Reactor 传播。

  4. 如果服务调用抛出异常,我们可以在WrapperObj上设置相应的错误结果。这类似于您使用AtomicReference,但没有创建额外的对象。

  5. 但是,这样的异常会导致 zipWith (6) 失败,因此如果发生这种情况,我们将替换 Optional.empty() 结果。

  6. zipWith 创建两个结果的元组

  7. 我们处理这些结果,替换

  8. 剩下的就是转换两个(非异常)结果:

    private WrapperObj transformResult(List<Obj> s1Result, List<Obj> s2Result, WrapperObj wrapper) {
        //perform your result transformation and
        //flesh out 'wrapper' with the results
        //if there was an exception, the 'wrapper' contains the corresponding exception values
        return wrapper;
    }
    

【讨论】:

    【解决方案3】:

    CompletableFutures 处理起来相当麻烦,但这里将是 IMO 一种更具功能性和反应性的方法。

    我们需要来自https://stackoverflow.com/a/30026710/1225328sequence 方法:

    static<T> CompletableFuture<List<T>> sequence(List<CompletableFuture<T>> com) {
        return CompletableFuture.allOf(com.toArray(new CompletableFuture<?>[0]))
                .thenApply(v -> com.stream()
                    .map(CompletableFuture::join)
                    .collect(Collectors.toList())
                );
    }
    

    然后,我使用Optional 来表示操作的状态,但Try monad 更适合(所以如果您的代码库中有这样的实用程序,请使用它 - Java 还没有自带):

    CompletableFuture<Optional<List<Object>>> future1 = service1.getSomething().thenApply(Optional::of).exceptionally(e -> {
        // log e
        return Optional.empty();
    });
    CompletableFuture<Optional<List<Object>>> future2 = service2.getSomething().thenApply(Optional::of).exceptionally(e -> {
        // log e
        return Optional.empty();
    });
    

    现在等待两个 future 并在结果可用时处理:

    CompletableFuture<List<Object>> mergedResults = sequence(Arrays.asList(future1, future2)).thenApply(results -> {
        Optional<List<Object>> result1 = results.get(0);
        Optional<List<Object>> result2 = results.get(1);
        if (result1.isEmpty() && result2.isEmpty()) {
            throw new CustomException(...);
        }
        // https://stackoverflow.com/a/18687790/1225328:
        return Stream.of(
                result1.map(Stream::of).orElseGet(Stream::empty),
                result2.map(Stream::of).orElseGet(Stream::empty)
        ).collect(Collectors.toList());
    });
    

    那么你最好直接返回mergedResults,让框架为你处理,这样你就不会阻塞任何线程,或者你可以在上面.get()(这将阻塞线程),这将抛出如果您的CustomException(或任何其他异常)被抛出(可在e.getCause() 中访问),则为ExecutionException


    如果您已经在使用 Project Reactor(或同等产品),这看起来会更简单,但想法大致相同。

    【讨论】:

    • 谢谢。为什么代码返回List&lt;Object&gt; 而不是List&lt;String&gt;?我认为即使将其更新为 List&lt;String&gt; 也不会编译。如果我理解最后一段,mergedResults 是否可以通过其ExecutionException 访问service1service2both 基础异常?另一个指针,以及我在我的 OP 中没有提到的一些东西,因为我认为我可以通过解决方案来解决它,但目前,我需要能够仅有条件地调用 service1 并静音 service2 直到它准备好。我不知道这个解决方案是否会有这种灵活性。
    • CompletableFuture 已经是这样时,调用 Try Monad 是很奇怪的。答案开头的sequence 方法已经足够了。然后,下一个代码 sn-p 不必要地将 Optional 添加到解决方案中,因此第三个代码 sn-p 是关于再次摆脱 Optional,以结束您已经在第一个 sn-p 处的位置。将任意异常更改为 CustomException 可以通过简单地链接 .exceptionally(t -&gt; { throw new CustomException(); }); 来完成,而无需在未发生异常时更改结果。
    • @Holger 我不确定如何在没有额外结构的情况下实现请求的逻辑 OP(“仅在两个期货都失败时失败,否则合并结果”)尝试(或者在我的情况下是可选的),因为我们需要两个结果才能知道整个过程是否应该被标记为失败。您介意用您的解决方案发布答案​​吗?我一直在尝试这种更简单的方法,但找不到出路。
    • @sp00m 我添加了答案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-04
    • 1970-01-01
    • 2022-08-13
    • 1970-01-01
    • 1970-01-01
    • 2021-07-03
    相关资源
    最近更新 更多