如何收集顺序调用异步API的结果?

时间:2018-09-07 05:08:00

标签: java asynchronous java-8 completable-future

我有一个异步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);
}

从复杂性的角度来看,我不喜欢这个。我们可以做得更好吗?

2 个答案:

答案 0 :(得分:1)

我还想在EA Async上进行介绍,因为它实现了对async/awaitinspired 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() /递归。唯一的要求是您的方法必须返回CompletionStageCompletableFuture

这真的很容易使异步代码

答案 1 :(得分:0)

在练习中,我制作了该算法的通用版本,但是由于需要,它相当复杂

  1. 调用服务的初始值(startFrom
  2. 服务调用本身(getNext()
  3. 用于累积中间值(offsets)的结果容器
  4. 一个累加器(offsets.addAll(response.getOffsets())
  5. 执行“递归”(response.hasMore())的条件
  6. 用于计算下一个输入(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,然后可以将servicenextInput函数合并。

但这会使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);
}