CompletableFuture.runAsync 吞咽异常

CompletableFuture.runAsync Swallowing Exceptions

早上好,

我不太了解 CompletableFutures(我是一名经验丰富的开发人员,但我并不觉得它们特别直观!)。

给定以下代码段:

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return gameService.deregister(playerName)
                    .exceptionally(t -> {
                      LOGGER.info("Could not deregister: {}", t.getMessage());
                      throw new CompletionException(t);
                    });
}

单元测试调用:

@Test
public void shouldCompleteExceptionallyForFailedLeave()
{
  var failFlow = new CompletableFuture<Void>();
  failFlow.completeExceptionally(new Exception("TestNonExistentPlayer"));
  when(mockedGameService.deregister(any(String.class))).thenReturn(failFlow);

  try
  {
    player.leaveGame(mockedGameService).toCompletableFuture().get();
    fail("Exception should have been thrown");
  }
  catch (Exception e)
  {
    assertEquals(Exception.class, e.getCause().getClass());
  }
  verify(mockedGameService, times(1)).deregister(any(String.class));
}

模拟 gameService.deregister(...) completeExceptionally 和 return Exception.

在上面的例子中,正如预期的那样,异常分支被触发,消息被记录,单元测试中的异常被捕获,即fail(...)断言没有被触发。

然而,当我想在离开游戏前运行一个CompletionStage时,例如:

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
                          .thenRun(() -> gameService.deregister(playerName)
                                                    .exceptionally(t -> {
                                                      LOGGER.info("Could not deregister: {}", t.getMessage());
                                                      throw new CompletionException(t);
                                                    }));
}

异常分支is仍然触发,但异常现在没有被测试方法捕获,即fail(...)断言is 触发。

我做错了什么?

非常感谢!

用你原来的定义

public CompletionStage<Void> leaveGame(GameService gameService)
{
  return gameService.deregister(playerName)
                    .exceptionally(t -> {
                      LOGGER.info("Could not deregister: {}", t.getMessage());
                      throw new CompletionException(t);
                    });
}

方法leaveGame 没有抛出异常但总是返回一个未来。调用者必须检查未来以确定封装操作是否失败。

同样,当您将相同的代码移动到 Runnable like

public CompletionStage<Void> leaveGame(GameService gameService)
{
    return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
        .thenRun(() -> gameService.deregister(playerName)
                                  .exceptionally(t -> {
                                    LOGGER.info("Could not deregister: {}", t.getMessage());
                                    throw new CompletionException(t);
                                  }));
}

Runnable不会抛出异常。仍然需要检查 gameService.deregister(…).exceptionally(…) 返回的 future 以确定它是否失败,但现在,您不是返回它,而是删除引用。

要创建一个未来,其完成取决于函数评估返回的未来,您需要 thenCompose:

public CompletionStage<Void> leaveGame(GameService gameService)
{
    return CompletableFuture.runAsync(() -> System.out.println("BLAH"))
        .thenCompose(voidArg -> gameService.deregister(playerName)
                                  .exceptionally(t -> {
                                    LOGGER.info("Could not deregister: {}", t.getMessage());
                                    throw new CompletionException(t);
                                  }));
}

所以现在你正在实施 Function<Void,CompletionStage<Void>> 而不是 Runnable 并且函数返回的阶段将用于完成 leaveGame.[=22= 返回的未来]