簡體   English   中英

跨線程保留 Java 堆棧跟蹤

[英]Preserve Java stack trace across threads

我使用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 個例外? 或者只是為了記錄目的將一個線程上下文保留在另一個線程上下文中?

根據我的評論,這實際上是我的想法。 請注意,我目前沒有辦法測試它。

您從父線程傳遞的是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 ) );
}

這將為您提供兩個堆棧跟蹤之間的邊界。

您可以使用從提交調用返回的Future<v>對象,然后調用get()方法,如果在任務執行期間發生任何異常,它將被重新拋出。

另一種選擇是為為您的ExecutorService創建線程的線程工廠自定義默認異常處理程序。 有關更多詳細信息,請參閱: Thread.UncaughtExceptionHandler

雖然我發現@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))
}

我傾向於做的是讓你調用的線程保存異常而不是拋出它。

然后啟動它的主線程可以輪詢它以查看是否發生了異常。 如果有,那么它可以獲得異常並拋出一個以它為原因的異常。

我認為您無法克隆異常。

作為一般規則,這種對異常的修改是壞消息,並且表明代碼中處理錯誤的一般方法可能存在一些問題。

這很難做到,因為你並不是真的打算這樣做。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM