跨线程保留 Java 堆栈跟踪

Preserve Java stack trace across threads

我正在使用 ExecutorService 异步发送邮件,所以有 class:

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 个异常?或者只是将一个线程上下文保留在另一个线程上下文中以进行日志记录?

我倾向于做的是让您正在调用的线程保存异常而不是抛出异常。

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

我认为您无法克隆异常。

一般来说,这种对异常的修补是个坏消息,表明代码中错误处理的一般方法可能存在一些问题。

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

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

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

您可以使用 Google Guava 库中的 ListableFuture class。参见 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!
  }
});

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

你从父线程传来的是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 ) );
}

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

虽然我发现@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))
}