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服务并更新
    martgageAccountId
    字段(REST返回MortgageInfo对象)
  • 调用事务REST服务并更新
    noOfTrans
    字段(REST返回
    Transactions
    object)
  • 调用地址REST服务并更新
    地址行
    字段(REST返回
    地址
    对象)
  • 调用链接REST服务并更新
    externalLink
    字段。(REST返回
    链接
    对象)
我希望以上所有调用并行进行,并且针对列表中的每个
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 each
supplyAsyc()
就像在@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