我有一个异步API,该API实际上通过分页返回结果
public CompletableFuture<Response> getNext(int startFrom);
每个Response
对象都包含一个startFrom
的偏移量列表和一个标志,该标志指示是否还有剩余元素,因此还有一个getNext()
要求发出。
我想编写一个遍历所有页面并检索所有偏移量的方法。我可以像这样以同步方式编写它
int startFrom = 0;
List<Integer> offsets = new ArrayList<>();
for (;;) {
CompletableFuture<Response> future = getNext(startFrom);
Response response = future.get(); // an exception stops everything
if (response.getOffsets().isEmpty()) {
break; // we're done
}
offsets.addAll(response.getOffsets());
if (!response.hasMore()) {
break; // we're done
}
startFrom = getLast(response.getOffsets());
}
换句话说,我们将getNext()
设为0的情况下调用startFrom
。如果引发异常,则会使整个过程短路。否则,如果没有偏移量,我们将完成。如果有偏移,我们将它们添加到主列表中。如果没有其他可提取的内容,我们将完成。否则,我们将startFrom
重置为我们获取的最后一个偏移并重复。
理想情况下,我想做到这一点而不会被CompletableFuture::get()
阻塞,并返回包含所有偏移量的CompletableFuture<List<Integer>>
。
我该怎么做?我如何组成期货以收集其结果?
我正在考虑“递归”(实际上不是在执行中,而是在代码中)
private CompletableFuture<List<Integer>> recur(int startFrom, List<Integer> offsets) {
CompletableFuture<Response> future = getNext(startFrom);
return future.thenCompose((response) -> {
if (response.getOffsets().isEmpty()) {
return CompletableFuture.completedFuture(offsets);
}
offsets.addAll(response.getOffsets());
if (!response.hasMore()) {
return CompletableFuture.completedFuture(offsets);
}
return recur(getLast(response.getOffsets()), offsets);
});
}
public CompletableFuture<List<Integer>> getAll() {
List<Integer> offsets = new ArrayList<>();
return recur(0, offsets);
}
从复杂性的角度来看,我不喜欢这个。我们可以做得更好吗?
答案 0 :(得分:1)
我还想在EA Async上进行介绍,因为它实现了对async/await(inspired from C#)的Java支持。所以我只是拿了您的初始代码,并进行了转换:
public CompletableFuture<List<Integer>> getAllEaAsync() {
int startFrom = 0;
List<Integer> offsets = new ArrayList<>();
for (;;) {
// this is the only thing I changed!
Response response = Async.await(getNext(startFrom));
if (response.getOffsets().isEmpty()) {
break; // we're done
}
offsets.addAll(response.getOffsets());
if (!response.hasMore()) {
break; // we're done
}
startFrom = getLast(response.getOffsets());
}
// well, you also have to wrap your result in a future to make it compilable
return CompletableFuture.completedFuture(offsets);
}
然后您必须instrument your code,例如通过添加
Async.init();
在main()
方法的开头。
我必须说:这真像魔术!
在后台,EA Async注意到方法中有一个Async.await()
调用,并将其重写为您处理所有thenCompose()
/ thenApply()
/递归。唯一的要求是您的方法必须返回CompletionStage
或CompletableFuture
。
这真的很容易使异步代码!
答案 1 :(得分:0)
在练习中,我制作了该算法的通用版本,但是由于需要,它相当复杂
startFrom
)getNext()
)offsets
)的结果容器offsets.addAll(response.getOffsets())
)response.hasMore()
)的条件getLast(response.getOffsets())
)的函数所以这给出了:
public <T, I, R> CompletableFuture<R> recur(T initialInput, R resultContainer,
Function<T, CompletableFuture<I>> service,
BiConsumer<R, I> accumulator,
Predicate<I> continueRecursion,
Function<I, T> nextInput) {
return service.apply(initialInput)
.thenCompose(response -> {
accumulator.accept(resultContainer, response);
if (continueRecursion.test(response)) {
return recur(nextInput.apply(response),
resultContainer, service, accumulator,
continueRecursion, nextInput);
} else {
return CompletableFuture.completedFuture(resultContainer);
}
});
}
public CompletableFuture<List<Integer>> getAll() {
return recur(0, new ArrayList<>(), this::getNext,
(list, response) -> list.addAll(response.getOffsets()),
Response::hasMore,
r -> getLast(r.getOffsets()));
}
通过将第一次调用的结果recur()
和initialInput
返回的CompletableFuture
替换为resultContainer
,可以简化accumulator
可以合并为单个Consumer
,然后可以将service
与nextInput
函数合并。
但这会使getAll()
更加复杂:
private <I> CompletableFuture<Void> recur(CompletableFuture<I> future,
Consumer<I> accumulator,
Predicate<I> continueRecursion,
Function<I, CompletableFuture<I>> service) {
return future.thenCompose(result -> {
accumulator.accept(result);
if (continueRecursion.test(result)) {
return recur(service.apply(result), accumulator, continueRecursion, service);
} else {
return CompletableFuture.completedFuture(null);
}
});
}
public CompletableFuture<List<Integer>> getAll() {
ArrayList<Integer> resultContainer = new ArrayList<>();
return recur(getNext(0),
result -> resultContainer.addAll(result.getOffsets()),
Response::hasMore,
r -> getNext(getLast(r.getOffsets())))
.thenApply(unused -> resultContainer);
}