Java CompletableFuture—并行运行多个rest调用并获得不同的结果
我有一个相当普遍或独特的要求。例如,我有以下Java CompletableFuture—并行运行多个rest调用并获得不同的结果,java,multithreading,spring-boot,java-8,completable-future,Java,Multithreading,Spring Boot,Java 8,Completable Future,我有一个相当普遍或独特的要求。例如,我有以下AccountDetails列表: 列表 除了bankAccountId之外,上述所有字段都是从外部REST服务调用中提取的。 我想并行调用所有REST服务并更新列表中的每个对象: 因此,它如下所示: 对于每个accountDetails 调用抵押REST服务并更新martgageAccountId字段(REST返回MortgageInfo对象) 调用事务REST服务并更新noOfTrans字段(REST返回Transactionsobject) 调
AccountDetails
列表:
列表
除了bankAccountId
之外,上述所有字段都是从外部REST服务调用中提取的。
我想并行调用所有REST服务并更新列表中的每个对象:
因此,它如下所示:
对于每个accountDetails
- 调用抵押REST服务并更新
字段(REST返回MortgageInfo对象)martgageAccountId
- 调用事务REST服务并更新
字段(REST返回noOfTrans
object)Transactions
- 调用地址REST服务并更新
字段(REST返回地址行
对象)地址
- 调用链接REST服务并更新
字段。(REST返回externalLink
对象)链接
AcccountDetails
对象。
如果有例外情况,我想做优雅地处理它。请注意,上面的每个REST服务都返回不同的自定义对象
我对如何通过CompletableFuture
链接实现这一点感到困惑。
不确定allOf
或thenCombine
(只需要两个),或thenCompose
应该使用以及如何将所有这些组合在一起
有什么例子/想法吗?既然您已经标记了
spring boot
,我想您应该使用它,并且您的服务是在spring框架中编写的。然后我提供了一个与spring框架相关的答案
AccountDetails accountDetails = new AccountDetails();
CompletableFuture.allOf(
CompletableFuture.
supplyAsync(() -> //CALL MORTAGE INFO REST, executor).
thenAccept(x -> {
accountDetails.setMortgageAccountId(x.getReqdField())
}).
handle(//HANDLE GRACEFULLY),
CompletableFuture.
supplyAsync(() -> //CALL SOME OTHER REST, executor).
thenAccept(x -> {
accountDetails.setNoOfTrans(x.getReqdField())
}).
handle(//HANDLE GRACEFULLY),
CompletableFuture.
supplyAsync(() -> //CALL SOME INFO REST, executor).
thenAccept(x -> {
accountDetails.setAddressLine(x.getReqdField())
}).
handle(//HANDLE GRACEFULLY),
CompletableFuture.
supplyAsync(() -> //CALL SOME OTHER REST, executor).
thenAccept(x -> {
accountDetails.setExternalLink(x.getReqdField())
}).
handle(//HANDLE GRACEFULLY),
).join();
首先,我创建了一个接口,用于将RESTAPI实现为异步的
public interface AsyncRestCall<T> {
/** this is a hypothetical method with hypothetical params!*/
CompletableFuture<T> call(String bankAccountId);
String type();
}
抵押贷款余额:
@Service
public class MortgageRest {
private RestTemplate restTemplate;
public MortgageRest(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public MortgageInfo service(String bankAccountId) {
return new MortgageInfo("123455" + bankAccountId);
}
}
对于其他rest服务,请执行此操作
@Service
public class TransactionService implements AsyncRestCall<Transactions> {
private final TransactionRest transactionRest;
public TransactionService(TransactionRest transactionRest) {
this.transactionRest = transactionRest;
}
@Override
public CompletableFuture<Transactions> call(String bankAccountId) {
return CompletableFuture.supplyAsync(transactionRest::service);
}
@Override
public String type() {
return "transactions";
}
}
现在,您需要访问所有asynchrestcall
实现。对于这个porpus,您可以声明一个类,如下所示:
@Service
public class RestCallHolder {
private final List<AsyncRestCall> asyncRestCalls;
public RestCallHolder(List<AsyncRestCall> asyncRestCalls) {
this.asyncRestCalls = asyncRestCalls;
}
public List<AsyncRestCall> getAsyncRestCalls() {
return asyncRestCalls;
}
}
我将负责将字段值提取到模型对象本身。
这里有三种替代解决方案,使用并行流、流和执行器,以及for循环和执行器 解决方案1:
accounts.parallelStream()
.<Runnable>flatMap(account -> Stream.of(account::updateMortgage, account::updateNoOfTrans,
account::updateAddressLine, account::updateExternalLink))
.map(RestRequest::new)
.forEach(RestRequest::run);
通用代码:
class RestRequest implements Runnable {
private final Runnable task;
RestRequest(Runnable task) {
this.task = task;
}
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
// A request failed. Others will not be canceled.
}
}
}
class AccountDetails {
String bankAccountId;
String mortgageAccountId;
Integer noOfTrans;
String addressLine;
String externalLink;
void fetchMortgage() {
mortgageAccountId = MortgageService.getMortgage(bankAccountId).getAccountId();
}
void fetchNoOfTrans() {
noOfTrans = TransactionService.getTransactions(bankAccountId).getAmount();
}
void fetchAddressLine() {
addressLine = AddressService.getAddress(bankAccountId).getLine();
}
void fetchExternalLink() {
externalLink = LinkService.getLinks(bankAccountId).getExternal();
}
}
如果我简单地将您的account类划分为:
class Account {
String fieldA;
String fieldB;
String fieldC;
Account(String fieldA, String fieldB, String fieldC) {
this.fieldA = fieldA;
this.fieldB = fieldB;
this.fieldC = fieldC;
}
}
然后,您可以使用CompletableFuture#allOf(…)
等待所有CompletableFuture的结果,每个字段更新一次,然后分别从这些未来检索结果。我们不能使用allOf
的结果,因为它不返回任何内容(void)
我们可以在中使用join,然后在apply
中使用,因为所有可完成的未来都是在这个阶段完成的。您可以修改上面的代码块以适应您的逻辑,例如更新字段而不是创建新对象。请注意,当可完成的未来异常完成时,上面的join()
会引发异常。在将未来提交给allOf(…)
之前,您可以将可完成的未来更改为handle()
,或者在使用join()
之前询问它是否已完成():
在一个完成阶段内更新字段的好处是,这些操作在同一个线程中完成,因此您不必担心并发修改。什么是
handle()
,其中包含什么?另外,我还有一个AccountDetails
列表,因此我将把上面的所有内容放在一个循环流中。这意味着,每个循环运行一个新线程,在每个线程下,大约有4个新线程运行?handle()->返回一个新的CompletionStage,当该阶段正常或异常完成时,该阶段的结果和异常作为提供函数的参数执行。是的,您可以运行帐户详细信息列表的循环,并使用上面给出的可完成未来填充它,每个循环运行不一定在不同的线程上运行。无论何时使用CompletableFuture.SupplySync或CompletableFuture.SupplySync,您都希望提供自己的执行器。使用默认的forkjoin池没有太多可用线程,并且几乎总是会导致调用备份。在allOf()
末尾的join()
与每个supplyAsyc()
的。此外,正如黄敏聪(Mincong Huang)在下面评论的那样,join()在每次SupplySync()之后都是最好避免的阻塞。我不同意获取外部数据以添加到模型上的责任。与下面相比,它的效率和性能如何?我会尽量避免为每个对象调用端点。如果您的后端服务上有批量请求,您应该遵从这些请求。因此,您将收集所有ID,然后向后端服务发出一个请求。@SamOrozco您是对的。但是这些调用是缓存的。因此,单独调用比批量调用更好,这就是为什么每当您使用CompletableFuture.supplyAsync
或CompletableFuture.supplyAsync
时,都要在单个记录中循环。您总是希望提供自己的执行器。使用默认的forkjoin池没有太多可用线程,并且几乎总是会导致调用备份。请帮助我理解。join()
vs at eachsupplyAsyc()
就像在@KevinRave中一样,正如您所说,join()
是一个阻塞操作。因此,我们应该尽可能避免使用join()
,以使整个逻辑无阻塞。如果我们在每次supplyAsync()
之后执行join()
,这些异步逻辑将变为同步并阻塞当前线程
@Service
public class AccountDetailService {
private final RestCallHolder restCallHolder;
public AccountDetailService(RestCallHolder restCallHolder) {
this.restCallHolder = restCallHolder;
}
public List<AccountDetail> update(List<AccountDetail> accountDetails) {
Map<String, Map<String, Object>> result = new HashMap<>();
List<AccountDetail> finalAccountDetails = new ArrayList<>();
accountDetails.forEach(accountDetail -> {
List<CompletableFuture> futures = restCallHolder.getAsyncRestCalls()
.stream()
.map(rest -> rest.call(accountDetail.getBankAccountId()))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.thenAccept(aVoid -> {
Map<String, Object> res = restCallHolder.getAsyncRestCalls()
.stream()
.map(rest -> new AbstractMap.SimpleEntry<>(rest.type(),
rest.call(accountDetail.getBankAccountId()).join()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
result.put(accountDetail.getBankAccountId(), res);
}
).handle((aVoid, throwable) -> {
return null; // handle the exception here
}).join();
}
);
accountDetails.forEach(accountDetail -> finalAccountDetails.add(AccountDetail.builder()
.bankAccountId(accountDetail.getBankAccountId())
.mortgageAccountId(((MortgageInfo) result.get(accountDetail.getBankAccountId()).get("mortgage")).getMortgageAccountId())
.noOfTrans(((Transactions) result.get(accountDetail.getBankAccountId()).get("transactions")).getNoOfTrans())
.build()));
return finalAccountDetails;
}
}
accounts.parallelStream()
.<Runnable>flatMap(account -> Stream.of(account::updateMortgage, account::updateNoOfTrans,
account::updateAddressLine, account::updateExternalLink))
.map(RestRequest::new)
.forEach(RestRequest::run);
Executor executor = Executors.newFixedThreadPool(PARALLELISM);
accounts.stream()
.<Runnable>flatMap(account -> Stream.of(account::updateMortgage, account::updateNoOfTrans,
account::updateAddressLine, account::updateExternalLink))
.map(RestRequest::new)
.forEach(executor::execute);
Executor executor = Executors.newFixedThreadPool(PARALLELISM);
for (AccountDetails account : accounts) {
execute(executor, account::updateMortgage);
execute(executor, account::updateNoOfTrans);
execute(executor, account::updateAddressLine);
execute(executor, account::updateExternalLink);
}
private static void execute(Executor executor, Runnable task) {
executor.execute(new RestRequest(task));
}
class RestRequest implements Runnable {
private final Runnable task;
RestRequest(Runnable task) {
this.task = task;
}
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
// A request failed. Others will not be canceled.
}
}
}
class AccountDetails {
String bankAccountId;
String mortgageAccountId;
Integer noOfTrans;
String addressLine;
String externalLink;
void fetchMortgage() {
mortgageAccountId = MortgageService.getMortgage(bankAccountId).getAccountId();
}
void fetchNoOfTrans() {
noOfTrans = TransactionService.getTransactions(bankAccountId).getAmount();
}
void fetchAddressLine() {
addressLine = AddressService.getAddress(bankAccountId).getLine();
}
void fetchExternalLink() {
externalLink = LinkService.getLinks(bankAccountId).getExternal();
}
}
class Account {
String fieldA;
String fieldB;
String fieldC;
Account(String fieldA, String fieldB, String fieldC) {
this.fieldA = fieldA;
this.fieldB = fieldB;
this.fieldC = fieldC;
}
}
Account account = CompletableFuture.allOf(cfA, cfB, cfC)
.thenApply(ignored -> {
String a = cfA.join();
String b = cfB.join();
String c = cfC.join();
return new Account(a, b, c);
}).join(); // or get(...) with timeout
CompletableFuture.allOf(cfA, cfB, cfC)
.thenRun(() -> {
if (!cfA.isCompletedExceptionally()) {
account.fieldA = cfA.join();
}
if (!cfB.isCompletedExceptionally()) {
account.fieldB = cfB.join();
}
if (!cfC.isCompletedExceptionally()) {
account.fieldC = cfC.join();
}
}).join(); // or get(...) with timeout