我正在用Java编写一个Play2应用程序服务方法,该方法应该执行以下操作。异步调用方法A,如果失败,则异步调用方法B.
为了说明假设服务调用的后端的这个接口:
public interface MyBackend {
CompletionStage<Object> tryWrite(Object foo);
CompletionStage<Object> tryCleanup(Object foo);
}
所以在我的服务方法中,我想返回一个可以完成这些的Future:
(注意:当然tryWrite()可以自行进行任何清理,这是一个用来说明问题的简化示例)
像这样调用后端的服务的实现对我来说似乎很难,因为CompletionStage.exceptionally()方法不允许Composing。
版本1:
public class MyServiceImpl {
public CompletionStage<Object> tryWriteWithCleanup(Object foo) {
CompletionStage<Object> writeFuture = myBackend.tryWrite(foo)
.exceptionally((throwable) -> {
CompletionStage<Object> cleanupFuture = myBackend.tryCleanup(foo);
throw new RuntimeException(throwable);
});
return writeFuture;
}
}
因此版本1以非阻塞方式调用tryCleanup(foo),但tryWriteWithCleanup()返回的CompletionStage不会等待cleanupFuture完成。如何更改此代码以从等待cleanupFuture完成的服务返回未来?
第2版:
public class MyServiceImpl {
public CompletionStage<Object> tryWriteWithCleanup(Object foo) {
final AtomicReference<Throwable> saveException = new AtomicReference<>();
CompletionStage<Object> writeFuture = myBackend
.tryWrite(foo)
.exceptionally(t -> {
saveException.set(t);
// continue with cleanup
return null;
})
.thenCompose((nil) -> {
// if no cleanup necessary, return
if (saveException.get() == null) {
return CompletableFuture.completedFuture(null);
}
return CompletionStage<Object> cleanupFuture = myBackend.tryCleanup(foo)
.exceptionally(cleanupError -> {
// log error
return null;
})
.thenRun(() -> {
throw saveException.get();
});
});
return writeFuture;
}
}
Version2使用外部AtomicReference来存储失败,并在另一个thenCompose()块中进行异步第二次调用(如果发生故障)。
我所有其他尝试这样做的结果都非常笨拙,以至于我不想在这里粘贴它们。
答案 0 :(得分:7)
不幸的是,CompletionStage
/ CompletableFuture
不提供具有组合的异常处理API。
您可以通过依靠handle()
并BiFunction
返回CompletionStage
来解决此问题。这将为您提供嵌套阶段(CompletionStage<CompletionStage<Object>>
),您可以使用&#34; 不需要&#34;使用compose(identity())
:
public CompletionStage<Object> tryWriteWithCleanup(Object foo) {
return myBackend.tryWrite(foo)
.handle((r, e) -> {
if (e != null) {
return myBackend.tryCleanup(foo)
.handle((r2, e2) -> {
// Make sure we always return the original exception
// but keep track of new exception if any,
// as if run in a finally block
if (e2 != null) {
e.addSuppressed(e2);
}
// wrapping in CompletionException behaves as if
// we threw the original exception
throw new CompletionException(e);
});
}
return CompletableFuture.completedFuture(r);
})
.thenCompose(Function.identity());
}
答案 1 :(得分:1)
您可能只是等待处理程序内的完成:
public CompletionStage<Object> tryWriteWithCleanup(Object foo) {
return myBackend.tryWrite(foo).exceptionally(throwable -> {
myBackend.tryCleanup(foo).toCompletableFuture().join();
throw new CompletionException(throwable);
});
}
这会将结果CompletionStage
的完成推迟到清理阶段的完成。使用CompletionException
作为包装将使包装对调用者透明。
然而,它有一些缺点。虽然框架可能在等待或产生补偿线程时利用线程,但如果它是工作线程,如果tryWrite
返回的阶段恰好在输入时exceptionally
已经完成,则被阻塞的线程可能是调用者线程}}。不幸的是,没有exceptionallyAsync
方法。您可以使用handleAsync
代替,但它会使代码复杂化,同时仍然感觉像是一个kludge。
此外,清理引发的异常可能会影响原始故障。
更清洁的解决方案可能会涉及更多:
public CompletionStage<Object> tryWriteWithCleanup(Object foo) {
CompletableFuture<Object> writeFuture = new CompletableFuture<>();
myBackend.tryWrite(foo).whenComplete((obj,throwable) -> {
if(throwable==null)
writeFuture.complete(obj);
else
myBackend.tryCleanup(foo).whenComplete((x,next) -> {
try {
if(next!=null) throwable.addSuppressed(next);
}
finally {
writeFuture.completeExceptionally(throwable);
}
});
});
return writeFuture;
}
这只是手动创建一个CompletableFuture
,允许控制它的完成,这可以直接由成功案例中链接到tryWrite
阶段的动作发生,也可以通过链接到特殊情况下的清理阶段。请注意,后者负责通过addSuppressed
链接可能的后续清理异常。