为什么将exception.printStackTrace()视为不良做法?


126

有一个很大材料这表明印刷异常的堆栈跟踪是不好的做法。

例如从Checkstyle中的RegexpSingleline检查:

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

但是,我正在努力寻找能够给出正当理由的任何地方,因为肯定可以使堆栈跟踪对于跟踪导致异常的原因非常有用。我知道的事情:

  1. 最终用户永远不应看到堆栈跟踪(出于用户体验和安全目的)

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

  3. 许多日志记录框架都会为您打印堆栈跟踪记录(我们不这样做,不行,我们无法轻松更改它)

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

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


12
作为必须定期进行故障排除的人,当出现问题时,我将永远不会忽略打印​​堆栈跟踪信息。是的,不向用户显示它,但是,是的,将其转储到错误日志中。
Paul Grime

4
您真的需要其他原因吗?我认为您已经很好地回答了自己的问题。但是,应在例外情况下打印stacktrace。:)
尼尔

Answers:


125

Throwable.printStackTrace()将堆栈跟踪写入System.errPrintStream。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() 它不是线程安全的

没有同步机制可以将堆栈跟踪的写入与System.err多个线程同时调用Throwable.printStackTrace()时的同步。要解决此问题,实际上需要您的代码在与关联的监视器上进行同步System.errSystem.out如果目标文件/设备相同,则还要进行同步),这对于支付日志文件的完整性来说是一笔不小的代价。举个例子,ConsoleHandlerand StreamHandler类负责在java.util.logging; 提供的日志记录工具中将日志记录追加到控制台。发布日志记录的实际操作是同步的-每个尝试发布日志记录的线程也必须获得与该日志记录关联的监视器上的锁StreamHandler实例。如果希望使用System.out/ 保证具有非交错日志记录System.err,则必须确保相同-消息以可序列化的方式发布到这些流中。

考虑到以上所有内容以及在Throwable.printStackTrace()实际中非常有用的非常受限的场景,通常会发现调用它是一种不好的做法。


扩展前面段落之一中的参数,Throwable.printStackTrace与写到控制台的记录器一起使用也是一个糟糕的选择。部分原因是由于记录器将在不同的监视器上进行同步,而您的应用程序(可能,如果您不需要交错的日志记录)将在不同的监视器上进行同步。当您在应用程序中使用两个写入相同目的地的不同记录器时,该参数也适用。


好答案,谢谢。但是,尽管我同意大多数时候它是否会产生噪音,但在某些时候它绝对至关重要。
克里斯·奈特

1
@克里斯是,有些时候,你不得不用System.out.printlnThrowable.printStackTrace,当然,需要开发商的判断。我有点担心缺少有关线程安全的部分。如果您查看大多数记录器实现,则会注意到它们同步记录日志记录的部分(甚至记录到控制台),尽管它们没有在System.err或上获取监视器System.out
Vineet Reynolds

2
我会错误地指出,所有这些原因都不适用于以PrintStream或PrintWriter对象为参数的printStackTrace替代吗?
ESRogs

1
@Geek,后者是值为2的进程文件描述符。前者只是一个抽象。
Vineet Reynolds

1
在printStackTrace()的JDK源代码中,它使用sync来锁定System.err PrintStream,因此它应该是线程安全的方法。
EyouGo

33

您在这里涉及多个问题:

1)绝不应向最终用户显示堆栈跟踪(出于用户体验和安全目的)

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

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

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

生成堆栈跟踪发生在创建/抛出异常时(这就是为什么要抛出异常要付出代价),而打印并不那么昂贵。实际上,您可以Throwable#fillInStackTrace()有效地重写自定义异常,从而使抛出异常几乎与简单的GOTO语句一样便宜。

3)许多日志记录框架会为您打印堆栈跟踪记录(我们不会,也不会,我们无法轻松更改它)

很好。这里的主要问题是:如果框架为您记录了异常,则不执行任何操作(但请确保已记录!)。如果要自己记录该异常,请使用LogbackLog4J之类的记录框架,不要将其放在原始控制台上因为很难控制它。

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

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

再次:SQLException正确记录(使用完整的堆栈跟踪,使用记录框架)并显示漂亮:“ 很抱歉,我们当前无法处理您的请求 ”消息。您是否真的认为用户对原因感兴趣?您是否看到了StackOverflow错误屏幕?这很幽默,但没有透露任何细节。但是,它确保用户可以对问题进行调查。

但是他立即给您打电话,您需要能够诊断问题。因此,您同时需要:适当的异常日志记录和用户友好的消息。


总结一下:始终记录异常(最好使用logging framework),但不要将异常公开给最终用户。仔细考虑一下GUI中的错误消息,仅在开发模式下显示堆栈跟踪。


您的答案就是我得到的答案。
Blasanka '17

“大多数'例外'情况”。很好
awwsmm '18

19

正如您所说的那样,第一件事printStackTrace()并不昂贵,因为在创建异常本身时就填充了堆栈跟踪。

想法是通过记录器框架传递记录的所有内容,以便可以控制记录。因此,不要使用printStackTrace,而要使用类似Logger.log(msg, exception);


11

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

此外,如果在一个catch块中执行的所有操作都是a,则有一种可能会怀疑未执行适当的异常处理e.printStackTrace。处理不当可能最多意味着一个问题将被忽略,而最糟糕的是该程序会继续以未定义或意外的状态执行。

让我们考虑以下示例:

try {
  initializeState();

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

continueProcessingAssumingThatTheStateIsCorrect();

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

在上面的代码中,应该已经捕获了异常并对其进行了适当的处理,以防止程序继续使用continueProcessingAssumingThatTheStateIsCorrect可能会导致问题的方法。

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

为什么这会成为问题?

糟糕的异常处理变得更加普遍的最大原因之一可能是由于Eclipse之类的IDE如何自动生成e.printStackTrace对异常处理执行代码的代码:

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

(以上是try-catchEclipse自动生成的实际值,用于处理InterruptedException抛出的异常Thread.sleep。)

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


1
这个。通常至少在方法级别上根本不捕获异常要比捕获,打印堆栈跟踪并继续进行就好,就像没有发生问题一样。
eis

10

我认为您列出的原因很全面。

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

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

上面的代码的问题在于,处理完全printStackTrace调用组成:异常未真正正确地处理,也不允许其转义。

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

最后,轻一点,就是上帝的完美例外


“不允许转义异常”-您是说异常是在堆栈中传播的吗?日志记录与printStackTrace()有何不同?
MasterJoe

5

printStackTrace()打印到控制台。在生产环境中,没有人观看过。Suraj是正确的,应将此信息传递给记录器。


有效的一点,尽管我们仔细观察了生产控制台的输出。
克里斯·奈特

您能通过仔细观察来解释您的意思吗?如何和谁?什么频率?
Oh Chin Boon

通过仔细观察,我应该在需要时说。它是一个滚动日志文件,如果我们的应用出现问题,它是几个调用端口之一。
克里斯·奈特

3

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

例如,当使用tomcat作为容器时,catalina.out


好点子。滥用printStackTrace()可能会破坏我们的日志文件,或者至少会充满大量无用的信息
克里斯·奈特

@ChrisKnight-您能推荐一篇文章解释日志文件如何被无用的信息炸毁吗?谢谢。
MasterJoe

3

这不是一个坏习惯,因为有关PrintStackTrace()的某些问题是“错误的”,但是却有“代码的味道”。大多数情况下,由于有人未能正确处理该异常,因此PrintStackTrace()调用在那里。一旦以适当的方式处理了异常,通常就不再关心StackTrace。

此外,在stderr上显示stacktrace通常仅在调试时有用,而在生产环境中则不可用,因为stderr经常无处可去。记录它更有意义。但是仅用记录异常替换PrintStackTrace()仍然会给您一个失败的应用程序,但它像没有发生任何事情一样保持运行。


0

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

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


0

为了避免@Vineet Reynolds提到的输出流纠结问题

您可以将其打印到标准输出: e.printStackTrace(System.out);

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.