跨线程保留Java堆栈跟踪

时间:2015-06-08 12:06:04

标签: java multithreading logging stack-trace

我使用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:还有另一种智能方法可以合并这两个例外吗?或者只是在另一个线程上下文中保留一个线程上下文以进行日志记录?

5 个答案:

答案 0 :(得分:9)

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

您从父帖子传递的内容是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 ) );
}

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

答案 1 :(得分:3)

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

另一个选择是为为ExecutorService创建线程的线程工厂自定义默认异常处理程序。有关详细信息,请参阅:Thread.UncaughtExceptionHandler

答案 2 :(得分:1)

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

答案 3 :(得分:0)

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

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

我认为你不能克隆异常。

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

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

答案 4 :(得分:0)

您可以使用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!
  }
});