为什么exception.printStackTrace()被认为是不良实践?

143
有很多材料表明,在异常情况下打印堆栈跟踪是不良做法。例如,来自Checkstyle中的RegexpSingleline检查:

此检查可用于[...]查找常见的不良实践,如调用ex.printStacktrace()

然而,我很难找到任何一个合理的原因,为什么打印堆栈跟踪是不良做法,因为堆栈跟踪在追踪引起异常的原因方面非常有用。我知道的一些事情包括:
以下是避免在代码中打印堆栈跟踪的其他原因:
1. 堆栈跟踪可能包含敏感信息,如文件路径、用户名和密码等。
2. 堆栈跟踪可能会向攻击者提供有关您的应用程序的详细信息,从而增加了安全风险。
3. 堆栈跟踪可能会使代码更难以阅读和理解,特别是对于新手来说。

18
作为经常需要解决问题的人,当出现错误时我绝不会漏掉打印堆栈信息。是的,不要将其显示给用户,但是一定要将它记录在错误日志中。 - Paul Grime
4
你真的需要其他理由吗?我认为你已经回答得很好了。但是,在异常情况下应该打印堆栈跟踪。 :) - Neil
1
Error Prone 现在会在您的代码中 警告您不要使用 .printStackTrace() :) - dimo414
1
为了避免@Vineet Reynolds提到的纠缠输出流问题,您可以将其打印到stdout:e.printStackTrace(System.out); - Traeyee
9个回答

142
Throwable.printStackTrace()方法将堆栈跟踪输出到System.err的PrintStream。JVM进程的System.err流和其基础标准的“错误”输出流可以通过以下方式进行重定向:
  • 调用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提供的日志记录工具中,发布日志记录的实际操作是同步的-每个尝试发布日志记录的线程也必须获取与StreamHandler实例关联的监视器上的锁定。如果想要使用System.out/System.err记录非交错的日志记录,必须确保以可串行化的方式将消息发布到这些流中。

考虑到以上所有内容以及Throwable.printStackTrace()实际上有很受限制的场景,通常情况下调用它是不好的做法。

延伸前面段落的论点,使用Throwable.printStackTrace配合写入到控制台的日志记录器也是一个不好的选择。部分原因是,日志记录器会在不同的监视器上同步,而您的应用程序(如果不想要交错的日志记录)将在不同的监视器上同步。如果在应用程序中使用写入到同一目的地的两个不同的日志记录器,该论点仍然适用。


1
非常好的答案,谢谢。然而,虽然我同意大多数时候它是噪音或不必要的,但有时它绝对是至关重要的。 - Chris Knight
1
@Chris 是的,有时候你不得不使用 System.out.printlnThrowable.printStackTrace,当然,需要开发者自己判断。我有点担心线程安全的部分被忽略了。如果你看大多数日志记录器的实现,你会注意到它们同步写入日志记录的部分(甚至是控制台),尽管它们没有获取 System.errSystem.out 的监视器。 - Vineet Reynolds
2
我是否可以指出,这些原因都不适用于接受PrintStream或PrintWriter对象作为参数的printStackTrace重载方法? - ESRogs
1
@Geek,后者是值为2的进程文件描述符。前者只是一个抽象概念。 - Vineet Reynolds
1
在 JDK 的 printStackTrace() 源代码中,它使用 synchronized 来锁定 System.err PrintStream,因此应该是一个线程安全的方法。 - EyouGo
显示剩余5条评论

34
你提到了多个问题:

1)堆栈跟踪信息不应该对终端用户可见(出于用户体验和安全考虑)。

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

  • 它们非常难以理解和阅读,这使应用程序看起来非常不友好。
  • 向终端用户显示堆栈跟踪信息可能会引入潜在的安全风险。如果我错了,请纠正我,PHP 实际上在堆栈跟踪信息中打印函数参数——这很棒,但也非常危险——如果你在连接数据库时遇到异常,你很可能会在堆栈跟踪信息中暴露密码等敏感信息。

2)生成堆栈跟踪信息是一个相对昂贵的过程(虽然在大多数“异常”情况下不太可能成为问题)

生成堆栈跟踪信息发生在创建/抛出异常时(这就是为什么抛出异常需要一定代价的原因),但打印并不那么昂贵。事实上,你可以用自定义异常重写 Throwable#fillInStackTrace() 方法,这样就可以让抛出异常的代价几乎和简单的 GOTO 语句一样小。

3)很多日志框架会为你打印堆栈跟踪信息(我们的框架不会,并且不容易更改)

非常好的观点。主要问题在于:如果框架为你记录了异常,什么都不要做(但一定要确保它这样做!)。如果你想自己记录异常,请使用像 Logback 或者 Log4J 这样的日志框架,而不是直接输出到控制台,因为很难控制控制台的输出。

使用日志框架,你可以轻松地将堆栈跟踪信息重定向到文件、控制台甚至发送到指定的电子邮件地址。而对于硬编码的 printStackTrace(),你只能接受标准输出流sysout 的形式。

4)打印堆栈跟踪信息并不等同于错误处理。它应该与其他信息记录和异常处理一起使用。

再强调一遍:正确记录SQLException(使用日志框架记录完整的堆栈跟踪),并显示友好的信息:“抱歉,我们目前无法处理您的请求”。你真的认为用户对原因感兴趣吗?你看过StackOverflow错误界面吗?它很幽默,但不会透露任何细节。但是它确保了用户问题将得到调查。
但他立即联系您,您需要能够诊断问题。因此,您需要正确记录异常和用户友好的消息。
总之:始终记录异常(最好使用日志框架),但不要将其暴露给最终用户。仔细考虑GUI中的错误消息,在开发模式下才显示堆栈跟踪。

你的答案就是我得到的答案。 - Blasanka
最“异常”的情况。不错。 - awwsmm

23

printStackTrace()方法并不像你所说的那样代价高昂,因为堆栈跟踪是在异常创建时填充的。

关键是通过日志记录器框架传递任何进入日志的内容,以便控制日志记录。因此,不要使用printStackTrace,而应该使用类似于Logger.log(msg, exception);的内容。


12

在异常处理中打印堆栈跟踪并不构成不良实践,但是仅在出现异常时打印堆栈跟踪可能是问题所在——通常情况下,仅Printing the exception's stack trace is not enough。

此外,在catch块中仅执行e.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();
}

(上面的try-catch是Eclipse自动生成的代码,用于处理由Thread.sleep引发的InterruptedException。)

对于大多数应用程序而言,仅将堆栈跟踪打印到标准错误可能是不够的。不当的异常处理可能会导致应用程序运行在意料之外的状态下,并可能导致意料之外和未定义的行为。


1
这个问题。在方法级别上,通常直接不捕获异常比捕获异常、打印堆栈跟踪并继续似乎没有问题更好。 - eis

10
我认为你列出的原因已经相当全面了。
我遇到过多次的一个特别糟糕的例子是这样的:
    try {
      // do stuff
    } catch (Exception e) {
        e.printStackTrace(); // and swallow the exception
    }

上述代码的问题在于处理方式完全由printStackTrace调用组成:异常没有得到适当处理,也没有被允许逃逸。
另一方面,通常情况下,我的代码中出现意外异常时,我总是记录堆栈跟踪。多年来,这一策略为我节省了大量调试时间。
最后,轻松一下,上帝的完美异常

“异常不允许逃逸” - 你的意思是异常被传播到堆栈上方了吗?日志记录和printStackTrace()有什么不同? - MasterJoe
1
死链,异常并不完美。 - john k
@john ktejik 试试这个 - M.Ed

5

printStackTrace()会在控制台上输出信息。在生产环境中,没有人会一直查看控制台。Suraj是正确的,应该将这些信息传递给记录器。


有道理,不过我们会仔细监控生产控制台的输出。 - Chris Knight
你能解释一下你所说的“仔细观察”是什么意思吗?怎么观察?由谁来观察?观察频率是多少? - OCB
通过仔细观察,我应该在需要时说出来。这是一个滚动日志文件,如果我们的应用程序出现问题,它是多个端口之一。 - Chris Knight

5
这并不是因为PrintStackTrace()本身有什么“问题”,而是它存在“代码异味”。通常,PrintStackTrace()被调用是因为某个人未能正确处理异常。一旦以适当的方式处理了异常,您通常就不再关心StackTrace了。
此外,在生产环境中,将堆栈跟踪显示在stderr上通常只在调试时有用,因为很多时候stderr无处可去。记录日志更有意义。但是,仅用记录异常替换PrintStackTrace()仍会使应用程序失败,但像什么都没发生一样继续运行。

4
在服务器应用程序中,堆栈跟踪会使您的stdout / stderr文件变得越来越大,并且充满了无用的数据,因为通常您没有上下文和时间戳等信息。例如,在使用Tomcat作为容器时,catalina.out文件可能会受到影响。

好观点。滥用printStackTrace()可能会使我们的日志文件爆炸,或者至少填充它们大量无用的信息。 - Chris Knight
@ChrisKnight - 你能否推荐一篇文章,解释一下日志文件如何被无用信息填满的问题?谢谢。 - MasterJoe

1
正如一些人在这里已经提到的那样,问题出在异常被吞噬了,如果你只是在catch块中调用e.printStackTrace(),它不会停止线程执行,并且将继续进行正常情况下的try块之后。
相反,你需要尝试从异常中恢复(如果可以恢复),或者抛出RuntimeException,或者将异常传递给调用者,以避免静默崩溃(例如,由于日志记录器配置不当)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接