如何创建异步堆栈跟踪?
更新 :Intellij IDEA的最新版本实现了我正在寻找的东西。 问题是如何在IDE之外实现这一点(因此我可以将异步堆栈跟踪转储到日志文件中),理想情况下不使用检测代理。
自从我将应用程序从同步模型转换为异步模型后,我遇到了调试失败的问题。
当我使用同步API时,我总是在exception堆栈跟踪中找到我的类,所以我知道从哪里开始查找是否出错。 使用异步API,我得到的堆栈跟踪不会引用我的类,也不会指示哪个请求触发了失败。
我将举一个具体的例子,但我对这类问题的一般解决方案感兴趣。
具体例子
我使用Jersey发出HTTP请求:
new Client().target("http://test.com/").request().rx().get(JsonNode.class);
其中rx()
表示请求应异步发生,直接返回CompletionStage
而不是JsonNode
。 如果此调用失败,我会得到此堆栈跟踪:
javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:1083) at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:883) at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767) at org.glassfish.jersey.internal.Errors.process(Errors.java:316) at org.glassfish.jersey.internal.Errors.process(Errors.java:298) at org.glassfish.jersey.internal.Errors.process(Errors.java:229) at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414) at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765) at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:456) at org.glassfish.jersey.client.JerseyCompletionStageRxInvoker.lambda$method$1(JerseyCompletionStageRxInvoker.java:70) at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
注意:
- 堆栈跟踪不引用用户代码。
- exception消息不包含有关触发错误的HTTP请求的上下文信息(HTTP方法,URI等)。
因此,我无法将exception追溯到其来源。
为什么会这样
如果你在引擎盖下挖掘,你会发现泽西岛正在调用 :
CompletableFuture.supplyAsync(() -> getSyncInvoker().method(name, entity, responseType))
对于rx()
调用。 由于供应商是由泽西岛建造的,因此没有参考用户代码。
我试过的
我试图针对一个无关的异步示例提交针对Jetty 的错误报告 ,随后因安全原因被拒绝。
相反,我一直在添加上下文信息如下:
makeHttpRequest().exceptionally(e -> { throw new RuntimeException(e); });
意思是,我在代码中的每个HTTP请求之后手动添加exceptionally()
。 Jersey抛出的任何exception都包含在引用我的代码的辅助exception中。 生成的堆栈跟踪如下所示:
java.lang.RuntimeException: javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed at my.user.code.Testcase.lambda$null$1(Testcase.java:25) at java.util.concurrent.CompletableFuture.uniExceptionally(CompletableFuture.java:870) ... 6 common frames omitted Caused by: javax.ws.rs.ForbiddenException: HTTP 403 Authentication Failed at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:1083) at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:883) at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767) at org.glassfish.jersey.internal.Errors.process(Errors.java:316) at org.glassfish.jersey.internal.Errors.process(Errors.java:298) at org.glassfish.jersey.internal.Errors.process(Errors.java:229) at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414) at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765) at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:456) at org.glassfish.jersey.client.JerseyCompletionStageRxInvoker.lambda$method$1(JerseyCompletionStageRxInvoker.java:70) at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590) ... 3 common frames omitted
我不喜欢这种方法,因为它容易出错并降低代码的可读性。 如果我错误地忽略了某些HTTP请求,我将最终得到一个模糊的堆栈跟踪,并花费大量时间跟踪它。
此外,如果我想在实用程序类后面隐藏这个技巧,那么我必须在CompletionStage
之外实例化一个exception; 否则,实用程序类将显示在堆栈跟踪而不是实际的调用站点中。 在CompletionStage
之外实例化exception是非常昂贵的,因为即使异步调用没有抛出exception,此代码也会运行。
我的问题
是否有一种强大的,易于维护的方法来向异步调用添加上下文信息?
或者,是否有一种有效的方法可以在没有此上下文信息的情况下跟踪堆栈跟踪返回其源?
看到这个问题在近一个月内没有得到任何答案,我将发布迄今为止我发现的最佳解决方案:
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() { } }
用法:
DebugCompletableFuture.wrap(CompletableFuture.supplyAsync(this::expensiveOperation));
好处:你将获得相对干净的异步堆栈跟踪。
缺点:每次创建未来时构造一个新的AsynchronousException
都非常昂贵。 具体来说,如果你生成很多未来,这会在堆上产生大量垃圾,GC开销变得明显。
我仍然希望有人会提出一个表现更好的方法。