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

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

例如,从正则表达式Singleline检查检查样式:

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

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

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

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

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

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

避免在代码中打印堆栈跟踪还有其他原因吗?


答案 1

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()

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

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


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


答案 2

您在这里触及多个问题:

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

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

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

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

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

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

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

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

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

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

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


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


推荐