为什么会认为 exction.printStackTrace()是不好的做法?

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

例如,来自 Checkstyle 的 RegexpSingleline 检查:

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

然而,我正在努力寻找任何地方,找到一个有效的理由,为什么堆栈跟踪肯定是非常有用的,在跟踪什么导致了异常。我所知道的事情:

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

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

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

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

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

142348 次浏览

首先,PrintStackTrace ()并不像您所说的那样昂贵,因为在创建异常本身时,堆栈跟踪会被填充。

其思想是通过日志记录器框架传递任何发送到日志的内容,以便可以控制日志记录。因此,不要使用 printStackTrace,而是使用类似 Logger.log(msg, exception);的工具

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

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

printStackTrace()打印到控制台。在制作环境中,从来没有人看这个。Suraj 是正确的,应该将此信息传递给记录器。

你在这里触及了多个问题:

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

是的,它应该能够诊断最终用户的问题,但最终用户不应该看到这些问题,原因有二:

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

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

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

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

说得好。这里的主要问题是: 如果框架为您记录异常,什么也不做(但要确保它这样做!)如果您想自己记录异常,使用日志框架,如 后退日志4J,不要将它们放在原始控制台上,因为很难控制它。

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

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

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

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


要结束这些操作: 一直都是日志异常(最好使用 日志记录框架) ,但不要将它们公开给最终用户。仔细考虑 GUI 中的错误消息,只在开发模式下显示堆栈跟踪。

打印异常的堆栈跟踪本身并不构成不好的做法,但是 只有在异常发生时打印堆栈跟踪可能是这里的问题——通常情况下,仅仅打印堆栈跟踪是不够的。

此外,如果在 catch块中执行的所有操作都是 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();
}

(上面是 Eclipse 自动生成的实际 try-catch,用于处理由 Thread.sleep抛出的 InterruptedException。)

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

我觉得你列的理由很全面。

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

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

上面代码的问题是处理由 printStackTrace调用的 完全相信组成: 异常没有得到正确的处理,也不允许转义。

另一方面,每当代码中出现意外异常时,我通常都会记录堆栈跟踪。这些年来,这个策略为我节省了大量的调试时间。

最后,说点轻松的 上帝的完美例外

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

此外,在 stderr 上显示堆栈跟踪通常只有在调试时才有用,而在生产环境中没有用,因为 stderr 通常不会发生任何变化。记录下来更有意义。但是仅仅将 PrintStackTrace ()替换为日志记录异常仍然会导致应用程序失败,但继续运行,就像什么也没发生一样。

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上进行同步) ,这对于日志文件的健全性来说是一个相当沉重的代价。举个例子,在 java.util.logging提供的日志设施中,ConsoleHandlerStreamHandler类负责将日志记录附加到控制台; 发布日志记录的实际操作是同步的——每个试图发布日志记录的线程也必须获得与 StreamHandler实例相关的监视器上的锁。如果您希望同样保证使用 System.out/System.err拥有非交错的日志记录,那么您必须确保同样的保证——消息以可序列化的方式发布到这些流中。

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


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

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

相反,您需要尝试从异常中恢复(如果它是可恢复的) ,或者抛出 RuntimeException,或者将异常冒泡发送给调用方,以避免无声崩溃(例如,由于不正确的日志记录器配置)。