簡體   English   中英

為什么 exception.printStackTrace() 被認為是不好的做法?

[英]Why is exception.printStackTrace() considered bad practice?

有很多的材料,這表明印刷異常的堆棧跟蹤是不好的做法。

例如,來自 Checkstyle 中的 RegexpSingleline 檢查:

此檢查可用於 [...] 查找常見的不良做法,例如調用 ex.printStacktrace()

但是,我正在努力尋找任何可以提供正當理由的地方,因為堆棧跟蹤對於跟蹤導致異常的原因肯定非常有用。 我所知道的事情:

  1. 堆棧跟蹤永遠不應對最終用戶可見(出於用戶體驗和安全目的)

  2. 生成堆棧跟蹤是一個相對昂貴的過程(盡管在大多數“特殊”情況下不太可能成為問題)

  3. 許多日志框架會為您打印堆棧跟蹤(我們的沒有,也不,我們不能輕易更改它)

  4. 打印堆棧跟蹤不構成錯誤處理。 它應該與其他信息記錄和異常處理相結合。

避免在代碼中打印堆棧跟蹤的其他原因是什么?

Throwable.printStackTrace()將堆棧跟蹤寫入System.err PrintStream。 System.err流和 JVM 進程的底層標准“錯誤”輸出流可以通過以下方式重定向

  • 調用System.setErr()改變System.err指向的目的地。
  • 或者通過重定向進程的錯誤輸出流。 錯誤輸出流可能被重定向到文件/設備
    • 其內容可能會被人員忽略,
    • 文件/設備可能無法進行日志輪換,推斷在歸檔文件/設備的現有內容之前需要重新啟動進程以關閉打開的文件/設備句柄。
    • 或者文件/設備實際上丟棄了所有寫入它的數據,就像/dev/null

從上面推斷,調用Throwable.printStackTrace()構成有效(不好/很好)的異常處理行為,僅

  • 如果您沒有在應用程序的整個生命周期內重新分配System.err
  • 如果您在應用程序運行時不需要日志輪換,
  • 如果接受/設計應用程序的日志記錄實踐是寫入System.err (和 JVM 的標准錯誤輸出流)。

在大多數情況下,不滿足上述條件。 人們可能不知道在 JVM 中運行的其他代碼,並且無法預測日志文件的大小或進程的運行時長,設計良好的日志記錄實踐將圍繞編寫“機器可解析”日志文件(一種記錄器中優選但可選的功能)在已知目的地,以幫助支持。

最后,應該記住Throwable.printStackTrace()的輸出肯定會與寫入System.err其他內容交錯(如果兩者都重定向到同一個文件/設備,甚至可能是System.out )。 這是一個必須處理的煩惱(對於單線程應用程序),因為在這種情況下,異常周圍的數據不容易解析。 更糟糕的是,多線程應用程序很可能會產生非常混亂的日志,因為Throwable.printStackTrace()不是線程安全的

當多個線程同時調用Throwable.printStackTrace()時,沒有同步機制將堆棧跟蹤的寫入同步到System.err 解決這個問題實際上需要您的代碼在與System.err相關聯的監視器上同步(如果目標文件/設備相同,還需要System.out ),這對於日志文件的完整性來說是相當沉重的代價。 舉個例子, ConsoleHandlerStreamHandler類負責將日志記錄附加到控制台,在java.util.logging提供的日志工具java.util.logging 發布日志記錄的實際操作是同步的——每個嘗試發布日志記錄的線程還必須獲取與StreamHandler實例關聯的監視器上的鎖。 如果您希望使用System.out / System.err獲得非交錯日志記錄的相同保證,則必須確保相同 - 消息以可序列化的方式發布到這些流。

考慮到上述所有情況,以及Throwable.printStackTrace()實際上有用的非常有限的場景,通常證明調用它是一種不好的做法。


擴展前面一段中的論點,將Throwable.printStackTrace與寫入控制台的記錄器結合使用也是一個糟糕的選擇。 這部分是由於記錄器會在不同的監視器上同步的原因,而您的應用程序會(可能,如果您不想要交錯的日志記錄)在不同的監視器上同步。 當您在應用程序中使用兩個不同的記錄器寫入同一目的地時,該論點也適用。

您在這里涉及多個問題:

1) 堆棧跟蹤永遠不應該對最終用戶可見(出於用戶體驗和安全目的)

是的,它應該可以用於診斷最終用戶的問題,但最終用戶不應該看到它們,原因有兩個:

  • 它們非常晦澀難懂,應用程序看起來非常不友好。
  • 向最終用戶顯示堆棧跟蹤可能會帶來潛在的安全風險。 如果我錯了,請糾正我,PHP 實際上在堆棧跟蹤中打印函數參數 - 很棒,但非常危險 - 如果您在連接到數據庫時遇到異常,您可能會在堆棧跟蹤中做什么?

2)生成堆棧跟蹤是一個相對昂貴的過程(盡管在大多數“例外”情況下不太可能成為問題)

在創建/拋出異常時會生成堆棧跟蹤(這就是拋出異常需要付出代價的原因),打印並不那么昂貴。 事實上,您可以在自定義異常中覆蓋Throwable#fillInStackTrace() ,從而有效地使拋出異常幾乎與簡單的 GOTO 語句一樣便宜。

3)許多日志框架會為您打印堆棧跟蹤(我們沒有,也沒有,我們不能輕易更改)

很好的觀點。 這里的主要問題是:如果框架為你記錄異常,什么都不做(但要確保它記錄了!)如果你想自己記錄異常,使用LogbackLog4J 之類的日志框架,不要把它們放在原始控制台上因為它很難控制。

使用日志框架,您可以輕松地將堆棧跟蹤重定向到文件、控制台,甚至將它們發送到指定的電子郵件地址。 使用硬編碼的printStackTrace()你必須忍受sysout

4) 打印堆棧跟蹤不構成錯誤處理。 它應該與其他信息記錄和異常處理相結合。

再次:正確記錄SQLException (使用完整的堆棧跟蹤,使用日志框架)並顯示:“抱歉,我們目前無法處理您的請求”消息。 你真的認為用戶感興趣的原因是什么? 你見過 StackOverflow 錯誤屏幕嗎? 它非常幽默,但沒有透露任何細節。 但是,它可以確保用戶對問題進行調查。

但是他立即給您打電話,您需要能夠診斷問題。 所以你需要兩者:適當的異常記錄和用戶友好的消息。


總結一下:總是記錄異常(最好使用日志框架),但不要將它們暴露給最終用戶。 仔細考慮 GUI 中的錯誤消息,僅在開發模式下顯示堆棧跟蹤。

第一件事printStackTrace()正如您所說的那樣並不昂貴,因為在創建異常本身時會填充堆棧跟蹤。

這個想法是通過記錄器框架傳遞任何進入日志的東西,以便可以控制日志記錄。 因此,不要使用 printStackTrace,只需使用類似Logger.log(msg, exception);東西Logger.log(msg, exception);

打印異常的堆棧跟蹤本身並不構成不良做法,但在發生異常時打印 stace 跟蹤可能是這里的問題——通常,僅打印堆棧跟蹤是不夠的。

此外,如果在catch塊中執行的所有操作都是e.printStackTrace ,則傾向於懷疑沒有執行正確的異常處理。 處理不當最多可能意味着一個問題被忽略了,最壞的情況是程序在未定義或意外的狀態下繼續執行。

例子

讓我們考慮以下示例:

try {
  initializeState();

} catch (TheSkyIsFallingEndOfTheWorldException e) {
  e.printStackTrace();
}

continueProcessingAssumingThatTheStateIsCorrect();

在這里,我們希望在繼續進行一些需要進行初始化的處理之前進行一些初始化處理。

在上面的代碼中,異常應該被捕獲並正確處理,以防止程序continueProcessingAssumingThatTheStateIsCorrect我們可以假設會導致問題的continueProcessingAssumingThatTheStateIsCorrect方法。

在許多情況下, e.printStackTrace()表明某些異常正在被吞下,並且允許處理繼續進行,就好像每次都沒有發生問題一樣。

為什么這成為一個問題?

糟糕的異常處理變得更加普遍的最大原因之一可能是 Eclipse 等 IDE 如何自動生成代碼,這些代碼將為異常處理執行e.printStackTrace

try {
  Thread.sleep(1000);
} catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

(以上是 Eclipse 自動生成的實際try-catch ,用於處理Thread.sleep拋出的InterruptedException 。)

對於大多數應用程序,僅將堆棧跟蹤打印到標准錯誤可能是不夠的。 在許多情況下,不正確的異常處理可能會導致應用程序在意外狀態下運行,並可能導致意外和未定義的行為。

我認為您列出的原因非常全面。

我不止一次遇到過的一個特別糟糕的例子是這樣的:

    try {
      // do stuff
    } catch (Exception e) {
        e.printStackTrace(); // and swallow the exception
    }

上面代碼的問題在於處理完全printStackTrace調用組成:異常沒有真正得到正確處理,也不允許逃逸。

另一方面,作為規則,每當我的代碼中出現意外異常時,我都會記錄堆棧跟蹤。 多年來,這項政策為我節省了大量調試時間。

最后,稍微說一下, 上帝的完美例外

printStackTrace()打印到控制台。 在生產環境中,沒有人會注意到這一點。 Suraj 是正確的,應該將此信息傳遞給記錄器。

這不是不好的做法,因為 PrintStackTrace() 有一些“錯誤”,但因為它是“代碼異味”。 大多數情況下,PrintStackTrace() 調用存在是因為有人未能正確處理異常。 一旦您以適當的方式處理異常,您通常不再關心 StackTrace。

此外,在 stderr 上顯示堆棧跟蹤通常僅在調試時有用,而不是在生產中,因為 stderr 通常無處可去。 記錄它更有意義。 但是僅僅用記錄異常來替換 PrintStackTrace() 仍然會給你留下一個失敗的應用程序,但像什么也沒發生一樣繼續運行。

在服務器應用程序中,堆棧跟蹤會破壞您的 stdout/stderr 文件。 它可能會變得越來越大,並且充滿了無用的數據,因為通常你沒有上下文,沒有時間戳等等。

例如 catalina.out 使用 tomcat 作為容器時

正如一些人在這里已經提到的那樣,問題在於吞咽異常,以防您只在catch塊中調用e.printStackTrace() 它不會停止線程執行,並且會像正常情況一樣在 try 塊之后繼續執行。

相反,您需要嘗試從異常中恢復(如果它是可恢復的),或者拋出RuntimeException ,或者將異常冒泡給調用者以避免靜默崩潰(例如,由於不正確的記錄器配置)。

暫無
暫無

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

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