为什么 exception.printStackTrace() 被认为是不良做法?

Why is exception.printStackTrace() considered bad practice?

提问人:Chris Knight 提问时间:9/19/2011 最后编辑:hallo545401Chris Knight 更新时间:9/19/2020 访问量:152331

问:

有很多材料表明,打印异常的堆栈跟踪是一种不好的做法。

例如,从 Checkstyle 中的 RegexpSingleline 检查:

此检查可用于查找常见的不良做法,例如调用 ex.printStacktrace()

但是,我正在努力找到任何给出正当理由的地方,因为堆栈跟踪肯定在跟踪导致异常的原因方面非常有用。我所知道的事情:

  1. 堆栈跟踪不应对最终用户可见(出于用户体验和安全目的)

  2. 生成堆栈跟踪是一个相对昂贵的过程(尽管在大多数“特殊”情况下不太可能成为问题)

  3. 许多日志记录框架会为您打印堆栈跟踪(我们的不会,不,我们不能轻易更改它)

  4. 打印堆栈跟踪不构成错误处理。它应该与其他信息日志记录和异常处理相结合。

还有哪些其他原因可以避免在代码中打印堆栈跟踪?

Java 异常 堆栈跟踪 printstacktrace

评论

18赞 Paul Grime 9/19/2011
作为一个必须定期排除故障的人,当出现问题时,我永远不会省略打印堆栈跟踪。是的,不要向用户显示它,但是是的,将其转储到错误日志中。
4赞 Neil 9/19/2011
你真的需要其他原因吗?我认为你已经很好地回答了你自己的问题。但是,堆栈跟踪应在异常情况下打印。:)
1赞 dimo414 1/23/2018
Error Prone 现在会警告您在代码:)中使用 .printStackTrace()
1赞 Traeyee 1/6/2020
为了避免 @Vineet Reynolds 提到的纠结输出流问题,您可以将其打印到 stdout:e.printStackTrace(System.out);

答:

23赞 Suraj Chandran 9/19/2011 #1

首先,printStackTrace() 并不昂贵,因为堆栈跟踪是在创建异常本身时填充的。

这个想法是通过记录器框架传递任何进入日志的内容,以便可以控制日志记录。因此,不要使用 printStackTrace,只需使用类似Logger.log(msg, exception);

4赞 Hajo Thelen 9/19/2011 #2

在服务器应用程序中,堆栈跟踪会破坏 stdout/stderr 文件。它可能会变得越来越大,并且充满了无用的数据,因为通常您没有上下文,也没有时间戳等等。

例如,使用 Tomcat 作为容器时的 catalina.out

评论

0赞 Chris Knight 9/19/2011
好点子。滥用 printStackTrace() 可能会破坏我们的日志记录文件,或者至少用大量无用的信息填充它
0赞 MasterJoe 12/20/2019
@ChrisKnight - 您能否推荐一篇文章来解释日志记录文件如何被无用的信息炸毁?谢谢。
5赞 Oh Chin Boon 9/19/2011 #3

printStackTrace()打印到控制台。在生产环境中,没有人会关注这一点。Suraj 是正确的,应该将此信息传递给记录器。

评论

0赞 Chris Knight 9/19/2011
有效的观点,尽管我们仔细观察了我们的生产控制台输出。
0赞 Oh Chin Boon 9/19/2011
你能解释一下你说的仔细观察是什么意思吗?如何和谁?以什么频率?
0赞 Chris Knight 9/19/2011
通过仔细观察,我应该在需要的时候说。它是一个滚动日志文件,如果我们的应用程序出现问题,它是几个调用端口之一。
34赞 Tomasz Nurkiewicz 9/19/2011 #4

您在这里遇到了多个问题:

1) 堆栈跟踪绝不应对最终用户可见(出于用户体验和安全目的)

是的,它应该可以用于诊断最终用户的问题,但最终用户不应看到它们,原因有两个:

  • 它们非常晦涩难懂且不可读,应用程序看起来非常用户不友好。
  • 向最终用户显示堆栈跟踪可能会带来潜在的安全风险。如果我错了,请纠正我,PHP 实际上在堆栈跟踪中打印函数参数 - 很棒,但非常危险 - 如果您在连接到数据库时遇到异常,您可能会在堆栈跟踪中做什么?

2) 生成堆栈跟踪是一个相对昂贵的过程(尽管在大多数“特殊”情况下不太可能成为问题)

在创建/抛出异常时生成堆栈跟踪(这就是为什么抛出异常是有代价的),打印成本并不高。事实上,您可以有效地覆盖自定义异常,使抛出异常几乎与简单的 GOTO 语句一样便宜。Throwable#fillInStackTrace()

3)许多日志框架会为你打印堆栈跟踪(我们的没有,不,我们不能轻易更改它)

非常好的观点。这里的主要问题是:如果框架为您记录了异常,则什么都不做(但要确保它这样做!如果您想自己记录异常,请使用 LogbackLog4J 等日志记录框架,不要将它们放在原始控制台上,因为很难控制它。

使用日志记录框架,您可以轻松地将堆栈跟踪重定向到文件、控制台,甚至将它们发送到指定的电子邮件地址。使用硬编码,您必须忍受 .printStackTrace()sysout

4) 打印堆栈跟踪不构成错误处理。它应该与其他信息日志记录和异常处理相结合。

再次:正确记录(使用日志记录框架使用完整堆栈跟踪)并显示 nice:“抱歉,我们目前无法处理您的请求”消息。你真的认为用户对原因感兴趣吗?你见过StackOverflow错误屏幕吗?它非常幽默,但没有透露任何细节。但是,它确保用户将对问题进行调查。SQLException

但是他会立即打电话给你,你需要能够诊断问题。因此,您需要两者兼而有之:适当的异常日志记录和用户友好的消息。


总而言之:始终记录异常(最好使用日志记录框架),但不要将它们暴露给最终用户。仔细考虑 GUI 中的错误消息,仅在开发模式下显示堆栈跟踪。

评论

0赞 Blasanka 8/6/2017
你的答案是我得到的答案。
0赞 awwsmm 1/31/2018
“大多数'例外'情况”。好。
12赞 coobird 9/19/2011 #5

打印异常的堆栈跟踪本身并不构成不良做法,但仅在发生异常时打印 stace 跟踪可能是这里的问题 - 通常,仅打印堆栈跟踪是不够的。

此外,如果一个块中执行的所有操作都是 .处理不当充其量可能意味着一个问题被忽略,最坏的情况是程序在未定义或意外状态下继续执行。catche.printStackTrace

让我们考虑以下示例:

try {
  initializeState();

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

continueProcessingAssumingThatTheStateIsCorrect();

在这里,我们想先进行一些初始化处理,然后再继续进行一些需要初始化的处理。

在上面的代码中,应该已经捕获并正确处理异常,以防止程序继续执行我们可以假设会导致问题的方法。continueProcessingAssumingThatTheStateIsCorrect

在许多情况下,表明某些异常正在被吞噬,并且允许处理继续进行,就好像没有发生任何问题一样。e.printStackTrace()

为什么这成为一个问题?

糟糕的异常处理变得越来越普遍的最大原因之一可能是由于 IDE(如 Eclipse)将自动生成代码,这些代码将执行异常处理:e.printStackTrace

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

(以上是 Eclipse 实际自动生成的,用于处理 抛出的 。try-catchInterruptedExceptionThread.sleep

对于大多数应用程序,仅将堆栈跟踪打印为标准错误可能还不够。在许多情况下,不正确的异常处理可能会导致应用程序在意外状态下运行,并可能导致意外和未定义的行为。

评论

1赞 eis 7/28/2017
这。很多时候,至少在方法级别上,根本不捕获异常比捕获、打印堆栈跟踪并继续,就好像没有发生问题一样要好。
10赞 NPE 9/19/2011 #6

我认为你列出的原因非常全面。

我不止一次遇到过一个特别糟糕的例子是这样的:

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

上面代码的问题在于处理完全由调用组成:异常没有得到正确处理,也不允许它逃逸。printStackTrace

另一方面,通常,每当代码中出现意外异常时,我总是记录堆栈跟踪。多年来,此策略为我节省了大量调试时间。

最后,在更轻松的音符上,上帝的完美例外

评论

0赞 MasterJoe 12/20/2019
“不允许异常逃逸” - 您的意思是异常在堆栈中传播吗?日志记录与 printStackTrace() 有何不同?
1赞 john k 1/20/2021
死链接,例外并不完美
0赞 M.Ed 6/25/2022
@john ktejik 试试这个
5赞 AVee 9/19/2011 #7

这不是坏做法,因为 PrintStackTrace() 有些“错误”,而是因为它是“代码味”。 大多数情况下,PrintStackTrace() 调用之所以存在,是因为有人未能正确处理异常。一旦你以适当的方式处理了异常,你通常就不再关心 StackTrace 了。

此外,在 stderr 上显示堆栈跟踪通常仅在调试时有用,而不是在生产中,因为 stderr 通常无处可去。记录它更有意义。但是,仅将 PrintStackTrace() 替换为记录异常仍然会让您看到一个失败但像什么都没发生一样继续运行的应用程序。

142赞 Vineet Reynolds 9/19/2011 #8

Throwable.printStackTrace()将堆栈跟踪写入 PrintStream。JVM 进程的流和底层标准“错误”输出流可以通过以下方式重定向System.errSystem.err

  • 调用 System.setErr(),这将更改 指向的目标。System.err
  • 或重定向进程的错误输出流。错误输出流可能会重定向到文件/设备
    • 其内容可能被人员忽略,
    • 文件/设备可能无法进行日志轮换,从而推断在存档文件/设备的现有内容之前,需要重新启动进程才能关闭打开的文件/设备句柄。
    • 或者文件/设备实际上会丢弃写入它的所有数据,就像 一样。/dev/null

从上面推断,调用构成有效(不好/很好)的异常处理行为,只有Throwable.printStackTrace()

  • 如果您在应用程序的整个生命周期内没有被重新分配,System.err
  • 如果在应用程序运行时不需要日志轮换,
  • 如果接受/设计了应用程序的日志记录实践,则写入(以及 JVM 的标准错误输出流)。System.err

在大多数情况下,上述条件都不满足。人们可能不知道 JVM 中正在运行的其他代码,也无法预测日志文件的大小或进程的运行时持续时间,而精心设计的日志记录实践将围绕在已知目标中写入“机器可解析”日志文件(记录器中可取但可选的功能)来帮助提供支持。

最后,应该记住,的输出肯定会与写入的其他内容交错(甚至可能两者都重定向到同一个文件/设备)。这是一个必须处理的烦恼(对于单线程应用程序),因为在这样的事件中,围绕异常的数据不容易解析。更糟糕的是,多线程应用程序很可能会生成非常令人困惑的日志,因为这不是线程安全的。Throwable.printStackTrace()System.errSystem.outThrowable.printStackTrace()

没有同步机制可以将堆栈跟踪的写入同步到多个线程同时调用时。解决此问题实际上需要您的代码在与关联的监视器上同步(如果目标文件/设备相同,并且还同步),这是为日志文件健全性付出的沉重代价。举个例子,and 类负责将日志记录追加到控制台,在 ;发布日志记录的实际操作是同步的 - 尝试发布日志记录的每个线程还必须获取与实例关联的监视器上的锁。如果您希望获得使用 / 的非交错日志记录的相同保证,则必须确保相同 - 消息以可序列化的方式发布到这些流。System.errThrowable.printStackTrace()System.errSystem.outConsoleHandlerStreamHandlerjava.util.loggingStreamHandlerSystem.outSystem.err

考虑到上述所有情况,以及实际有用的非常有限的场景,通常事实证明,调用它是一种不好的做法。Throwable.printStackTrace()


扩展前面一段中的参数,与写入控制台的记录器结合使用也是一个糟糕的选择。这在一定程度上是由于记录器将在不同的监视器上同步,而您的应用程序将(可能,如果您不想要交错的日志记录)在不同的监视器上同步。当您在应用程序中使用两个不同的记录器写入同一目标时,该参数也适用。Throwable.printStackTrace

评论

1赞 Chris Knight 9/19/2011
很好的答案,谢谢。然而,虽然我同意大多数时候它的噪音或没有必要,但有些时候它绝对关键。
1赞 Vineet Reynolds 9/19/2011
@Chris 是的,有时你不能不使用,当然,需要开发人员的判断。我有点担心错过了关于线程安全的部分。如果您查看大多数记录器实现,您会注意到它们同步写入日志记录的部分(甚至同步到控制台),尽管它们不会在 或 上获取监视器。System.out.printlnThrowable.printStackTraceSystem.errSystem.out
2赞 ESRogs 4/17/2013
我注意到这些原因都不适用于将 PrintStream 或 PrintWriter 对象作为参数的 printStackTrace 重写,我会错吗?
1赞 Vineet Reynolds 1/21/2014
@Geek,后者是值为 2 的进程文件描述符。前者只是一个抽象。
1赞 EyouGo 3/27/2018
在 printStackTrace() 的 JDK 源代码中,它使用 synchronized 来锁定 System.err PrintStream,因此它应该是一个线程安全的方法。
1赞 humkins 7/28/2017 #9

正如一些人在这里已经提到的那样,问题在于吞咽除外,以防万一您只是调用块。它不会停止线程执行,并且会在 try 块之后继续,就像在正常情况下一样。e.printStackTrace()catch

取而代之的是,您需要尝试从异常中恢复(如果它是可恢复的),或者抛出 ,或者将异常冒泡给调用者以避免静默崩溃(例如,由于记录器配置不正确)。RuntimeException