UPDATE: The latest version of Intellij IDEA implements exactly what I\'m looking for. The question is how to implement this outside of the IDE (so I can to
Seeing as this question has not received any answers in almost a month, I'm going to post the best solution I've found to date:
DebugCompletableFuture.java:
/**
* A {@link CompletableFuture} that eases debugging.
*
* @param the type of value returned by the future
*/
public final class DebugCompletableFuture extends CompletableFuture
{
private static RunMode RUN_MODE = RunMode.DEBUG;
private static final Set CLASS_PREFIXES_TO_REMOVE = ImmutableSet.of(DebugCompletableFuture.class.getName(),
CompletableFuture.class.getName(), ThreadPoolExecutor.class.getName());
private static final Set> EXCEPTIONS_TO_UNWRAP = ImmutableSet.of(AsynchronousException.class,
CompletionException.class, ExecutionException.class);
private final CompletableFuture delegate;
private final AsynchronousException asyncStacktrace;
/**
* @param delegate the stage to delegate to
* @throws NullPointerException if any of the arguments are null
*/
private DebugCompletableFuture(CompletableFuture delegate)
{
requireThat("delegate", delegate).isNotNull();
this.delegate = delegate;
this.asyncStacktrace = new AsynchronousException();
delegate.whenComplete((value, exception) ->
{
if (exception == null)
{
super.complete(value);
return;
}
exception = Exceptions.unwrap(exception, EXCEPTIONS_TO_UNWRAP);
asyncStacktrace.initCause(exception);
filterStacktrace(asyncStacktrace, element ->
{
String className = element.getClassName();
for (String prefix : CLASS_PREFIXES_TO_REMOVE)
if (className.startsWith(prefix))
return true;
return false;
});
Set newMethods = getMethodsInStacktrace(asyncStacktrace);
if (!newMethods.isEmpty())
{
Set oldMethods = getMethodsInStacktrace(exception);
newMethods.removeAll(oldMethods);
if (!newMethods.isEmpty())
{
// The async stacktrace introduces something new
super.completeExceptionally(asyncStacktrace);
return;
}
}
super.completeExceptionally(exception);
});
}
/**
* @param exception an exception
* @return the methods referenced by the stacktrace
* @throws NullPointerException if {@code exception} is null
*/
private Set getMethodsInStacktrace(Throwable exception)
{
requireThat("exception", exception).isNotNull();
Set result = new HashSet<>();
for (StackTraceElement element : exception.getStackTrace())
result.add(element.getClassName() + "." + element.getMethodName());
for (Throwable suppressed : exception.getSuppressed())
result.addAll(getMethodsInStacktrace(suppressed));
return result;
}
/**
* @param the type returned by the delegate
* @param delegate the stage to delegate to
* @return if {@code RUN_MODE == DEBUG} returns an instance that wraps {@code delegate}; otherwise, returns {@code delegate}
* unchanged
* @throws NullPointerException if any of the arguments are null
*/
public static CompletableFuture wrap(CompletableFuture delegate)
{
if (RUN_MODE != RunMode.DEBUG)
return delegate;
return new DebugCompletableFuture<>(delegate);
}
/**
* Removes stack trace elements that match a filter. The exception and its descendants are processed recursively.
*
* This method can be used to remove lines that hold little value for the end user (such as the implementation of utility functions).
*
* @param exception the exception to process
* @param elementFilter returns true if the current stack trace element should be removed
*/
private void filterStacktrace(Throwable exception, Predicate elementFilter)
{
Throwable cause = exception.getCause();
if (cause != null)
filterStacktrace(cause, elementFilter);
for (Throwable suppressed : exception.getSuppressed())
filterStacktrace(suppressed, elementFilter);
StackTraceElement[] elements = exception.getStackTrace();
List keep = new ArrayList<>(elements.length);
for (StackTraceElement element : elements)
{
if (!elementFilter.test(element))
keep.add(element);
}
exception.setStackTrace(keep.toArray(new StackTraceElement[0]));
}
@Override
public CompletableFuture thenApply(Function super T, ? extends U> fn)
{
return wrap(super.thenApply(fn));
}
@Override
public CompletableFuture thenApplyAsync(Function super T, ? extends U> fn)
{
return wrap(super.thenApplyAsync(fn));
}
@Override
public CompletableFuture thenApplyAsync(Function super T, ? extends U> fn, Executor executor)
{
return wrap(super.thenApplyAsync(fn, executor));
}
@Override
public CompletableFuture thenAccept(Consumer super T> action)
{
return wrap(super.thenAccept(action));
}
@Override
public CompletableFuture thenAcceptAsync(Consumer super T> action)
{
return wrap(super.thenAcceptAsync(action));
}
@Override
public CompletableFuture thenAcceptAsync(Consumer super T> action, Executor executor)
{
return wrap(super.thenAcceptAsync(action, executor));
}
@Override
public CompletableFuture thenRun(Runnable action)
{
return wrap(super.thenRun(action));
}
@Override
public CompletableFuture thenRunAsync(Runnable action)
{
return wrap(super.thenRunAsync(action));
}
@Override
public CompletableFuture thenRunAsync(Runnable action, Executor executor)
{
return wrap(super.thenRunAsync(action, executor));
}
@Override
public CompletableFuture thenCombine(CompletionStage extends U> other,
BiFunction super T, ? super U, ? extends V> fn)
{
return wrap(super.thenCombine(other, fn));
}
@Override
public CompletableFuture thenCombineAsync(CompletionStage extends U> other,
BiFunction super T, ? super U, ? extends V> fn)
{
return wrap(super.thenCombineAsync(other, fn));
}
@Override
public CompletableFuture thenCombineAsync(CompletionStage extends U> other,
BiFunction super T, ? super U, ? extends V> fn,
Executor executor)
{
return wrap(super.thenCombineAsync(other, fn, executor));
}
@Override
public CompletableFuture thenAcceptBoth(CompletionStage extends U> other,
BiConsumer super T, ? super U> action)
{
return wrap(super.thenAcceptBoth(other, action));
}
@Override
public CompletableFuture thenAcceptBothAsync(CompletionStage extends U> other,
BiConsumer super T, ? super U> action)
{
return wrap(super.thenAcceptBothAsync(other, action));
}
@Override
public CompletableFuture thenAcceptBothAsync(CompletionStage extends U> other,
BiConsumer super T, ? super U> action,
Executor executor)
{
return wrap(super.thenAcceptBothAsync(other, action, executor));
}
@Override
public CompletableFuture runAfterBoth(CompletionStage> other, Runnable action)
{
return wrap(super.runAfterBoth(other, action));
}
@Override
public CompletableFuture runAfterBothAsync(CompletionStage> other, Runnable action)
{
return wrap(super.runAfterBothAsync(other, action));
}
@Override
public CompletableFuture runAfterBothAsync(CompletionStage> other, Runnable action, Executor executor)
{
return wrap(super.runAfterBothAsync(other, action, executor));
}
@Override
public CompletableFuture applyToEither(CompletionStage extends T> other, Function super T, U> fn)
{
return wrap(super.applyToEither(other, fn));
}
@Override
public CompletableFuture applyToEitherAsync(CompletionStage extends T> other, Function super T, U> fn)
{
return wrap(super.applyToEitherAsync(other, fn));
}
@Override
public CompletableFuture applyToEitherAsync(CompletionStage extends T> other, Function super T, U> fn,
Executor executor)
{
return wrap(super.applyToEitherAsync(other, fn, executor));
}
@Override
public CompletableFuture acceptEither(CompletionStage extends T> other, Consumer super T> action)
{
return wrap(super.acceptEither(other, action));
}
@Override
public CompletableFuture acceptEitherAsync(CompletionStage extends T> other, Consumer super T> action)
{
return wrap(super.acceptEitherAsync(other, action));
}
@Override
public CompletableFuture acceptEitherAsync(CompletionStage extends T> other, Consumer super T> action,
Executor executor)
{
return wrap(super.acceptEitherAsync(other, action, executor));
}
@Override
public CompletableFuture runAfterEither(CompletionStage> other, Runnable action)
{
return wrap(super.runAfterEither(other, action));
}
@Override
public CompletableFuture runAfterEitherAsync(CompletionStage> other, Runnable action)
{
return wrap(super.runAfterEitherAsync(other, action));
}
@Override
public CompletableFuture runAfterEitherAsync(CompletionStage> other, Runnable action, Executor executor)
{
return wrap(super.runAfterEitherAsync(other, action, executor));
}
@Override
public CompletableFuture thenCompose(Function super T, ? extends CompletionStage> fn)
{
return wrap(super.thenCompose(fn));
}
@Override
public CompletableFuture thenComposeAsync(Function super T, ? extends CompletionStage> fn)
{
return wrap(super.thenComposeAsync(fn));
}
@Override
public CompletableFuture thenComposeAsync(Function super T, ? extends CompletionStage> fn,
Executor executor)
{
return wrap(super.thenComposeAsync(fn, executor));
}
@Override
public CompletableFuture exceptionally(Function fn)
{
return wrap(super.exceptionally(fn));
}
@Override
public CompletableFuture whenComplete(BiConsumer super T, ? super Throwable> action)
{
return wrap(super.whenComplete(action));
}
@Override
public CompletableFuture whenCompleteAsync(BiConsumer super T, ? super Throwable> action)
{
return wrap(super.whenCompleteAsync(action));
}
@Override
public CompletableFuture whenCompleteAsync(BiConsumer super T, ? super Throwable> action,
Executor executor)
{
return wrap(super.whenCompleteAsync(action, executor));
}
@Override
public CompletableFuture handle(BiFunction super T, Throwable, ? extends U> fn)
{
return wrap(super.handle(fn));
}
@Override
public CompletableFuture handleAsync(BiFunction super T, Throwable, ? extends U> fn)
{
return wrap(super.handleAsync(fn));
}
@Override
public CompletableFuture handleAsync(BiFunction super T, Throwable, ? extends U> fn,
Executor executor)
{
return wrap(super.handleAsync(fn, executor));
}
@Override
public boolean complete(T value)
{
return delegate.complete(value);
}
@Override
public boolean completeExceptionally(Throwable ex)
{
return delegate.completeExceptionally(ex);
}
}
RunMode.java:
/**
* Operational modes.
*/
public enum RunMode
{
/**
* Optimized for debugging problems (extra runtime checks, logging of the program state).
*/
DEBUG,
/**
* Optimized for maximum performance.
*/
RELEASE
}
AsynchronousException.java
/**
* Thrown when an asynchronous operation fails. The stacktrace indicates who triggered the operation.
*/
public final class AsynchronousException extends RuntimeException
{
private static final long serialVersionUID = 0L;
public AsynchronousException()
{
}
}
Usage:
DebugCompletableFuture.wrap(CompletableFuture.supplyAsync(this::expensiveOperation));
Upside: you'll get relatively clean asynchronous stack traces.
Downside: Constructing a new AsynchronousException every time a future is created is extremely expensive. Specifically, if you're generating a lot of futures, this generates a lot of garbage on the heap and the GC overhead becomes noticeable.
I am still hopeful that someone will come up with a better-performing approach.