【问题标题】:Preserve Java stack trace across threads跨线程保留 Java 堆栈跟踪
【发布时间】:2015-08-22 21:28:56
【问题描述】:

我使用ExecutorService异步发送邮件,所以有一个类:

class Mailer implements Runnable { ...

处理发送。任何被捕获的异常都会被记录下来,例如(匿名):

javax.mail.internet.AddressException: foo is bar
    at javax.mail.internet.InternetAddress.checkAddress(InternetAddress.java:1213) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:1091) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:633) ~[mail.jar:1.4.5]
    at javax.mail.internet.InternetAddress.parse(InternetAddress.java:610) ~[mail.jar:1.4.5]
    at mycompany.Mailer.sendMail(Mailer.java:107) [Mailer.class:?]
    at mycompany.Mailer.run(Mailer.java:88) [Mailer.class:?]
    ... suppressed 5 lines
    at java.lang.Thread.run(Thread.java:680) [?:1.6.0_35]

不是很有帮助 - 我需要查看调用导致所有这些的 ExecutorService 的堆栈跟踪。我的解决方案是创建一个空的Exception 并将其传递给Mailer

executorService.submit(new Mailer(foo, bar, new Exception()));
...
// constructor
public Mailer(foo, bar, Exception cause) { this.cause = cause; ...

现在在出现异常的情况下,我想从另一个线程记录问题本身及其原因:

try {
  // send the mail...
} catch (Throwable t) {
  LOG.error("Stuff went wrong", t);
  LOG.error("This guy invoked us", cause);
}

这很好用,但会产生两个日志。我想将tcause 组合成一个异常并记录该异常。在我看来,t 导致了cause,所以使用cause.initCause(t) 应该是正确的方法。并且有效。我看到了完整的堆栈跟踪:从调用源一直到 AddressException

问题是,initCause() 只工作一次然后崩溃。 问题1:我可以克隆Exception吗?我会克隆 cause 并每次都用 t 初始化它。

我试过t.initCause(cause),但它马上就崩溃了。

问题 2: 有没有另一种聪明的方法来结合这 2 个例外?或者只是将一个线程上下文保留在另一个线程上下文中以进行日志记录?

【问题讨论】:

  • 从异常中获取堆栈跟踪并使用它怎么样?
  • @RealSkeptic 所以我尝试了这个t.setStackTrace(ArrayUtils.addAll(t.getStackTrace(), cause.getStackTrace()));,它似乎有效。这不太好,例如,我很高兴看到线程边界以某种方式突出显示(“由:”是理想的),但它可以工作。把它写下来作为答案,这样我就可以接受了:)
  • “我可以克隆Exception吗?” 你可以通过序列化它并立即反序列化它。它很笨重,但很有效。
  • @vektor,你可以使用Exception e = new Exception(t); e.setStackTrace(cause.getStackTrace()); LOG.error("Stuff went wrong", e); 这样你应该有一个单独的原因。
  • 它们是独立的字段。 setStackTrace 的 javadoc 解释了为什么允许它:docs.oracle.com/javase/7/docs/api/java/lang/…

标签: java multithreading logging stack-trace


【解决方案1】:

我倾向于让你调用的线程保存异常而不是抛出它。

然后启动它的主线程可以轮询它以查看是否发生异常。如果有,那么它可以获取异常并以它作为原因抛出一个。

我认为你不能克隆异常。

作为一般规则,这种对异常的修补是坏消息,并且表明代码中错误处理的一般方法可能存在一些问题。

这很难做到,因为你并不是真的打算这样做。

【讨论】:

  • 我相信我的问题很笼统:跨线程边界跟踪堆栈跟踪。有没有完善的解决方案?
  • 我同意。这个问题是完全合法的,而且很常见。我的方法是我以前用来解决它的一种方法,并且是一种行之有效的方法。我敢肯定还有其他人,但我的强烈意见是,试图通过破解异常来解决它是一种糟糕的代码气味。
【解决方案2】:

您可以使用提交调用返回的Future<v> 对象,然后调用get() 方法,如果在任务执行过程中发生任何异常,它将被重新抛出。

另一个选项是为线程工厂自定义默认异常处理程序,该处理程序为您的ExecutorService 创建线程。详情见:Thread.UncaughtExceptionHandler

【讨论】:

    【解决方案3】:

    您可以使用 Google Guava 库中的 ListableFuture 类。见https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained

    ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
    ListenableFuture<Explosion> explosion = service.submit(new Callable<Explosion>() {
      public Explosion call() {
        return pushBigRedButton();
      }
    });
    Futures.addCallback(explosion, new FutureCallback<Explosion>() {
      // we want this handler to run immediately after we push the big red button!
      public void onSuccess(Explosion explosion) {
        walkAwayFrom(explosion);
      }
      public void onFailure(Throwable thrown) {
        battleArchNemesis(); // escaped the explosion!
      }
    });
    

    【讨论】:

    • 我仍然需要以保留两个堆栈的方式将thrown 登录到onFailure,我该怎么做?
    【解决方案4】:

    根据我的评论,这实际上是我的想法。请注意,我目前没有办法对其进行测试。

    您从父线程传递的内容是New Exception().getStackTrace()。或者更好的是,正如@Radiodef 评论的那样,Thread.currentThread().getStackTrace()。所以它基本上是一个StackTraceElement[] 数组。

    现在,你可以有类似的东西:

    public class CrossThreadException extends Exception {
    
        public CrossThreadException( Throwable cause, StackTraceElement[] originalStackTrace ) {
    
            // No message, given cause, no supression, stack trace writable
            super( null, cause, false, true );
    
            setStackTrace( originalStackTrace );
        }
    }
    

    现在在你的 catch 子句中你可以这样做:

    catch ( Throwable cause ) {
       LOG( "This happened", new CrossThreadException( cause, originalStackTrace ) );
    }
    

    这将为您提供两个堆栈跟踪之间的边界。

    【讨论】:

    • 你也可以通过Thread.currentThread().getStackTrace()而不是new Exception().getStackTrace()
    • @Radiodef 谢谢,已将其添加到答案中。
    • 您甚至不必将其作为参数传递。由于构造函数是在父线程上调用的,因此您只需在构造函数中设置堆栈跟踪字段:private StackTraceElement[] originalStackTrace = Thread.currentThread().getStackTrace(); 就在那里作为字段初始值设定项
    【解决方案5】:

    虽然我发现@RealSkeptic 的回答很有用,但我不喜欢输出。我认为这是“颠倒的”,所以这是我的解决方案,它为您提供以下输出:

    1. 子线程中的异常
    2. 子线程中的原因
    3. 父线程中的“原因”(包括线程名称)
    com.example.MyException: foo is bar
        at com.example.foo.Bar(Bar.java:23)
        ...
    Caused by: java.net.UnknownHostException: unknown.example.com
        at com.example.net.Client(Client.java:123)
        ...
    Caused by: com.example.CrossThreadException: Thread: main
        com.example.thread.MyExecutor.execute(MyExecutor.java:321)
        ...
    
    public class CrossThreadException extends Exception {
        public CrossThreadException(CrossThreadException parentThreadException) {
            this(null, null, parentThreadException);
        }
    
        public CrossThreadException(Throwable currentThreadCause, String skipPackage, CrossThreadException parentThreadException) {
            // No message, given cause, no supression, stack trace writable
            super(getMessageByCurrentThreadCause(currentThreadCause), parentThreadException);
            if (currentThreadCause != null) {
                if (skipPackage != null) {
                    final StackTraceElement[] stackTrace = currentThreadCause.getStackTrace();
                    Pair<Integer, Integer> startEnd = StackTraceHelper.stackTraceStartEnd(stackTrace, skipPackage, 0);
                    setStackTrace(Arrays.copyOfRange(stackTrace, startEnd.getFirst(), startEnd.getSecond()));
                } else {
                    setStackTrace(currentThreadCause.getStackTrace());
                }
            }
        }
    
        private static String getMessageByCurrentThreadCause(Throwable currentThreadCause) {
            return currentThreadCause != null
                    ? String.format("Thread: %s - %s: %s", Thread.currentThread().getName(), currentThreadCause.getClass().getSimpleName(), currentThreadCause.getMessage())
                    : String.format("Thread: %s", Thread.currentThread().getName());
        }
    }
    
    class ThreadHelper {
        public static final ThreadLocal<CrossThreadException> parentThreadException = new ThreadLocal<>();
    
        public static CrossThreadException getParentThreadException() {
            return parentThreadException = parentThreadException.get();
        }
    
        public static Throwable getCrossThreadException(Throwable cause) {
            CrossThreadException parentThreadException = getParentThreadException();
            if (parentThreadException == null) {
                return cause;
            }
    
            Throwable c = cause;
            while (c.getCause() != null && c.getCause() != c) {
                c = c.getCause();
            }
            c.initCause(parentThreadException);
            return cause;
        }
    }
    
    class MyExecutor extends ThreadPoolExecutor {
        @Override
        public void execute(Runnable command) {
            CrossThreadException parentThreadException = new CrossThreadException(ThreadHelper.getParentThreadException());
            super.execute(wrap(command, parentThreadException));
        }
    
        public static Runnable wrap(final Runnable runnable, final CrossThreadException parentThreadException) {
            return () -> {
                try {
                    ThreadHelper.parentThreadException.set(parentThreadException);
                    runnable.run();
                } finally {
                    ThreadHelper.parentThreadException.set(null);
                }
            };
        }
    }
    

    用法:使用 MyExecutor 时,您可以在子线程中捕获并记录异常:

    try {
        ...
    } catch (Throwable t) {
        log.error("Caught an exception", ThreadHelper.getCrossThreadException(t))
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-18
      • 2013-01-17
      • 1970-01-01
      • 2011-07-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多