陈同学
微服务
Accelerator
About
# 归零你Java异常知识的时候到了 > 原文:[It's Time to Unlearn Everything You Know About Java Exceptions](https://blog.takipi.com/the-truth-behind-the-big-exceptions-lie/) by Alex Zhitnisky > 翻译:[陈同学](https://chenyongjun.vip/) > 译者注:本文精髓我并未完全理解,只是因为作者对异常有一定的造诣,同时标题吸引人,所以暂时先翻译。 **Java语言中滥用最为严重的功能当属异常功能,原因如下** 为了此文,采访我们博客的老朋友 [Avishai Ish-Shalom](https://il.linkedin.com/in/nukemberg) ,他是一位经验丰富的系统架构师,他与我们简单沟通了当前Java应用中异常的状态。下面是我们发现的一些事情: ## 异常的定义远远偏离了normal 让我们看看来自 [Java官网文档](https://docs.oracle.com/javase/tutorial/essential/exceptions/) 的引用: ``` 异常是一个发生在程序运行中并且破坏了指令正常执行流程的事件. ``` 实践中,大多数应用中指令的normal 流程充满了会引起 **"normal中断"** 的 **"normal"** 异常。 一个误传在多数应用中快速增长,以为只要抛出异常、打印日志,然后归档并加以分析就万事大吉,但事实上这些动作毫无意义。 这个误传,除了对系统增加不必要的压力之外,还会使你丧失对真正重要异常的感知。 想象一下,一个电商应用正发生一个严重异常,并发出信号告知某个地方已出错而且已经造成了影响,信号显示:100个用户无法结账。现在,这个严重的异常被无数没用的 **"normal异常"覆盖**,可你却需要尝试了解到底发生了什么。 例如:多数应用有一个 "normal"级别的的错误event,在下面的截图中,我们可以看到每小时大约发生4K个event。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/05/25/cc6915706d6d405faffad9263b57b8c6.png) > [Overops](https://www.overops.com/) 异常趋势分析仪表板 如果幸运,一个新异常发生时可能在上图中以 **尖峰形状** 将自己展示出来,就像上图中一个 IllegalStateException 大约在凌晨一点左右成百上千次的发生,因此我们可以立马知道到底是什么导致了 **尖峰形状**。 上图中绿色的线条反映了event总量,其他颜色的线条反映了特定异常以及打印出来的 ERROR和WARING日志。 来自异常的风险往往低频、量小,但是致命的异常实例往往埋藏在那些 **"normal"** 级别的异常中。 ## 你在谈论的 "normal" 异常到底是什么? 不像那些需要代码修复的真正错误,现在的异常往往反映了一些无法采取任何措施的场景。考虑下这两种任何有经验的工程师都可以预见的场景: * **业务Errors**:用户可能做任何业务流程所不允许的事情,就像form表单上的电话号码验证,尽管前端会进行非空等相关验证,但NumberFormatException依然是发生量位居第二的异常,数据来自于我们最近从超过1B的生产环境做的调查([The top 10 exceptions](https://blog.takipi.com/the-top-10-exceptions-types-in-production-java-applications-based-on-1b-events/))。 * **系统Errors**:一些超出你控制的事情,任何操作时会出现问题的事情。例如:当尝试访问一个文件时却没有访问权限。 另一方面,真正的异常是一些你在编码时无法意识到的事情,例如:OutOfMemoryException。甚至是一个可能导致意外发生的NullPointerException,异常是一些需要你采取行动去真正解决的问题。 ## 异常就是设计用来搞破坏的 > 译者注:原文是为 Exceptions are designed to crash & burn,翻译时拿捏不准 **未捕获异常** 会kill你的线程,如果重要线程被kill而其他线程又因等待这个线程而卡住,严重时这将会导致应用宕机或者使得应用进入一种 "僵尸状态"。有的应用知道怎么处理这种情况,但大多数应用并不具备这个能力。 Java中异常的主要目的是帮助你捕获Bug并解决Bug。异常用于debug,这也是从应用的角度来说,异常为何会尽可能包含更多信息的原因。 当应用流程发生跳转时,会产生另一个问题是 状态不一致,这比GOTO 语句还要糟糕。它们有些相同的毛病: * 打断了程序的正常处理流程 * 导致很难跟踪和理解接下来会发生什么 * 即使有finally块也会很难cleanup * 太重量级,不像GOTO语句起码会携带着所有的stack和其他额外信息 ## 使用 "error" 流程而不是异常 如果你尝试用异常代替应用逻辑来处理那些可以预见的场景,那么你陷入了和大多数Java应用一样的麻烦。 可预见的异常并不是真正的异常。Scala特性中有个有趣的情景 — 不用异常来处理errors. 下面是Scala的官方文档的一个小例子: ```scala import scala.util.{Success, Failure} val f: Future[List[String]] = Future { session.getRecentPosts } f onComplete { case Success(posts) => for (post <- posts) println(post) case Failure(t) => println("An error has occured: " + t.getMessage) } ``` 上述代码中`f`方法的内部可能抛出异常,但是它们已经被妥善处理且不会泄漏到外部。可能的失败情景已经明确由 `f onComplete`分支来处理,而且执行代码非常容易。 在Java 8中,可以使用 **CompletableFuture** 功能,虽然*completeExceptionally()* 不是特别优雅,但我们依然可以使用。 ## The plot gets thicker with APIs > 译者注:暂时没有很好的翻译方式 比方说,我们有个用于访问数据库的library,那么这个DB library如何向外展示其异常?请记住:这个library也可能抛出一般性错误,例如:java.net.UnknowHostException或NullPointerException. JDBC的包装library就是一个会出错的真实例子,它只会抛出一个普通的DBException却不会给你机会了解到底发生了什么。发生异常时也许一切OK,可能只是一个JDBC连接错误,但也许你需要改代码来修正错误。 一个通用的解决方案是在DB library中使用一个基础异常(例如:DBException),library中的异常可以继承这个基础异常,这种方式允许使用library的用户可以用一个 `try块`捕获所有library中的错误。 但系统错误导致的library异常又该怎么处理呢?通用的处理方式是对library内部发生的所有异常进行封装。因此,像无法解析DNS地址这种问题(这首先更像是个系统错误,其次才是library中的错误),library需要捕获这个异常并且以更高级别的异常将其重新抛出,从而使得library的使用者知道如何捕获它。`Try-catch`的噩梦是一些包装着其他异常的嵌套异常。 ## 你能为之做点什么呢? 如果从头开始开发,避免不必要的异常还是比较简单,但是大多数情况并非如此。对于一个既存系统,比如5年前的应用,你需要做大量修修补补的无趣工作(而且前提是你获准修复这些问题)。 理想情况下,我们希望所有异常都是可操作的,也就是说,它会驱动一些行为以防止异常再次发生,而不是简单的告诉我们发生了一些异常。 总而言之,不可操作的异常会导致混乱: * 性能问题 * 稳定性问题 * 监控和日志分析问题 * 关键是它会埋藏掉真正你想看到和想采取行动的异常 解决方案是:努力处理掉这些异常并且开发更有意义的控制流程。另一个富有创造性的方案是变更日志级别,如果不是可操作的异常,不要打印错误日志。这是一个很棒的方案,但是也会耗掉你80%的时间。 ## 最后的想法 底线是:如果不是需要代码修复的异常,你根本就不需要浪费时间在它上面。 这篇文章是基于 Avishai的一次叫 "可操作异常的异常"的简短讲话。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
归零你Java异常知识的时候到了
文章链接:
https://chenyongjun.vip/articles/36
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力