问题
I am working on chaining CompletableFuture to adapt an exception. While I have something that is working, I don't understand why it works.
@Test
public void futureExceptionAdapt() throws ExecutionException, InterruptedException {
class SillyException extends Exception { }
class AdaptedException extends Exception { AdaptedException(SillyException silly) { } }
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
sleepForThreeSeconds();
if (true)
throw new CompletionException(new SillyException());
return 5;
})
.thenApplyAsync(val -> val * 10);
CompletableFuture<Integer> futureAdaptException = future.exceptionally((t) -> {
if (t instanceof CompletionException && t.getCause() instanceof SillyException) {
System.out.println("adapt SillyException to AdaptedException");
SillyException silly = (SillyException) t.getCause();
future.obtrudeException(new AdaptedException(silly));
}
return null;
});
try {
future.get();
fail("future should have failed with an exception");
} catch (ExecutionException e) {
assertTrue("got exception: " + getCauseClass(e),
e.getCause() instanceof AdaptedException);
}
// I am not sure why the above succeeds
// because I did not call futureAdaptException.get()
// According to the IDE futureAdaptException is an unused variable at this point
assertTrue("expect futureAdaptException to have failed with AdaptedException but instead the result is: " + futureAdaptException.get(),
futureAdaptException.isCompletedExceptionally());
}
private static void sleepForThreeSeconds() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
}
}
private static String getCauseClass(Throwable t) {
if (t.getCause() == null) return "null";
return t.getCause().getClass().getName();
}
First question is why is futureAdaptException
called when I simply call future.get()
?
Secondly, is there any way to get futureAdaptException.get()
to fail with the desired exception? Because I don't want to create a dummy object if I don't have to. You cannot modify futureAdaptException
from the lambda.
Or maybe there is a better way to adapt exceptions. Or maybe there is a variant of exceptionally()
that keeps the object in an exception stage (seems the return null
causes futureAdaptException
to put the object into normal stage with value null
). Or maybe we shouldn't be adapting exceptions in CompletableFuture
s.
@Test
public void futureStrangeBehaviorFromChaining1() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("sleep for 3 seconds then return 5");
sleepForThreeSeconds();
return 5;
});
future.thenApplyAsync(val -> {
System.out.println("multiply by 3");
return val*3;
});
sleepForFiveSeconds();
assertEquals(5, future.get().intValue());
sleepForFiveSeconds();
assertEquals(5, future.get().intValue());
}
@Test
public void futureStrangeBehaviorFromChaining2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("sleep for 3 seconds then return 5");
sleepForThreeSeconds();
return 5;
})
.thenApplyAsync(val -> {
System.out.println("multiply by 3");
return val*3;
});
sleepForFiveSeconds();
assertEquals(15, future.get().intValue());
sleepForFiveSeconds();
assertEquals(15, future.get().intValue());
}
回答1:
futureAdaptException
is not “called” when you call future.get()
. What happens is that you created it with future.exceptionally()
so it will automatically be triggered when future
completes exceptionally.
So even though futureAdaptException
is unused (so you could remove the variable), exceptionally()
still has a side effect.
The CompletableFuture
that you get from exceptionally()
will succeed or fail depending on what you do in the passed-in function. If you want it to fail, you can still throw an exception again:
CompletableFuture<Integer> futureAdaptException = future.exceptionally((t) -> {
if (t instanceof CompletionException && t.getCause() instanceof SillyException) {
System.out.println("adapt SillyException to AdaptedException");
SillyException silly = (SillyException) t.getCause();
final AdaptedException ex = new AdaptedException(silly);
future.obtrudeException(ex);
throw new CompletionException(ex);
}
return null;
});
Note that you should probably avoid using obtrudeException()
as this is non-deterministic. If fact, I am surprised that your first assert succeeds¹. The CompletableFuture
returned by exceptionally()
would complete with the same result as the original if it succeeds, so you should work with that one instead.
¹ I definitely think this is due to a bug in the JDK. If you add a sleepForThreeSeconds()
in exceptionally()
, the test still passes. However, if you add a sleep for more than 3s before future.get()
the assert fails and you get the original exception. If you call get()
before completion, it appears to wait for the exceptionally()
to execute as well. I have posted this question to understand this better.
来源:https://stackoverflow.com/questions/44578803/proper-way-to-adapt-an-exception-with-completablefuture