【发布时间】: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 的第二次服务调用将在 ReactorWebClient中进行调用,但是,在收到来自WebClient的结果后需要执行一些逻辑——这就是为什么我认为我有在单独的@Async线程中为新服务处理这些逻辑(如果这不正确,请告知)。 -
@sp00m 这是否也会捕获
ServInf.doSomething()抛出的异常?get()似乎只抛出了一些异常。 -
请进一步澄清:
WrapperObj的定义是什么?如果幸福的道路只涉及其中一个结果,那么您为什么要让这些服务相互竞争?两者都不是更可取:负载平衡(智能?);总是喜欢一个,只有在失败时才调用另一个(最容易实现?); ……?关于我之前的评论,你可以在额外的 Reactor 转换中执行额外的逻辑(事实上你应该,以保持一切反应并防止整个事情在你用完线程时卡住)。
标签: java exception completable-future