【问题标题】:Why is exception.printStackTrace() considered bad practice?为什么 exception.printStackTrace() 被认为是不好的做法?
【发布时间】:2011-11-20 02:58:39
【问题描述】:

there 中有很多 material 表明打印异常的堆栈跟踪是不好的做法。

例如从 Checkstyle 中的 RegexpSingleline 检查:

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

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

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

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

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

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

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

【问题讨论】:

  • 作为一个必须定期排除故障的人,我从不会在出现问题时省略打印堆栈跟踪。是的,不要向用户显示它,但是是的,将其转储到错误日志中。
  • 你真的需要其他理由吗?我认为你已经很好地回答了你自己的问题。但是,堆栈跟踪应该在异常异常中打印。 :)
  • Error Prone 现在将在您的代码中 warn you about using .printStackTrace() :)
  • 为了避免@Vineet Reynolds 提到的输出流问题,您可以将其打印到标准输出:e.printStackTrace(System.out);

标签: java exception stack-trace printstacktrace


【解决方案1】:

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 与写入控制台的记录器结合使用也是一个糟糕的选择。这部分是因为记录器会在不同的监视器上同步,而您的应用程序会(可能,如果您不想要交错的日志记录)在不同的监视器上同步。当您在应用程序中使用两个不同的记录器写入同一目的地时,该论点也适用。

【讨论】:

  • 很好的答案,谢谢。然而,虽然我同意大多数时候它的噪音或没有必要,但有些时候它是绝对关键的。
  • @Chris 是的,有时你不得不使用System.out.printlnThrowable.printStackTrace,当然,需要开发人员的判断。我有点担心错过了关于线程安全的部分。如果您查看大多数记录器实现,您会注意到它们会同步写入日志记录的部分(甚至到控制台),尽管它们不会在 System.errSystem.out 上获取监视器。
  • 我是否会错误地指出这些原因都不适用于将 PrintStream 或 PrintWriter 对象作为参数的 printStackTrace 覆盖?
  • @Geek,后者是值为2的进程文件描述符。前者只是一个抽象。
  • 在printStackTrace()的JDK源码中,它使用synchronized来锁定System.err PrintStream,所以应该是线程安全的方法。
【解决方案2】:

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

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

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

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

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

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

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

非常好的观点。这里的主要问题是:如果框架为你记录了异常,什么都不做(但要确保它确实记录了!)在原始控制台上,因为它很难控制。

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

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

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

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


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

【讨论】:

  • 你的答案就是我得到的答案。
  • “大多数'例外'情况”。不错。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

    此外,如果在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。)

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

    【讨论】:

    • 这个。通常只是不捕获异常,至少在方法级别上,比捕获、打印堆栈跟踪并继续,就好像没有发生问题一样好。
    【解决方案5】:

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

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

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

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

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

    最后,说点轻松的,God's Perfect Exception

    【讨论】:

    • "异常不允许逃逸" - 你的意思是异常向上传播到堆栈吗?日志记录与 printStackTrace() 有何不同?
    • 死链接,异常并不完美
    【解决方案6】:

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

    【讨论】:

    • 有效点,虽然我们仔细观察我们的生产控制台输出。
    • 你能解释一下你所说的仔细观察是什么意思吗?如何和谁?频率是多少?
    • 仔细观察,我应该在需要的时候说。它是一个滚动日志文件,如果我们的应用出现问题,它是几个调用端口之一。
    【解决方案7】:

    这是一个不错的做法,因为 PrintStackTrace() 有一些“错误”,但因为它是“代码气味”。 大多数时候 PrintStackTrace() 调用是因为有人未能正确处理异常。一旦以适当的方式处理异常,您通常就不再关心 StackTrace 了。

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

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

      • 好点。滥用 printStackTrace() 可能会破坏我们的日志文件,或者至少在其中填充大量无用信息
      • @ChrisKnight - 你能否推荐一篇文章来解释日志文件如何被无用的信息炸毁?谢谢。
      【解决方案9】:

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

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

      【讨论】:

        猜你喜欢
        • 2010-10-22
        • 2010-11-04
        • 2022-07-05
        • 2012-05-18
        • 2013-10-27
        相关资源
        最近更新 更多