Java 异常分析

本文是对以下内容的分析:

  • Java异常设计

  • Java 异常分类

  • Java异常可以告诉什么问题

  • Java异常处理最佳实践

Java Exception 是为了处理应用程序的异常行为而创建的类。在本文中,将解释如何使用 Java Exception 类以及如何在考虑现有 Java Exception 设计的情况下创建异常结构。Java 异常概念是 Java 中的重要里程碑之一,每个开发人员都必须知道它。

Java 异常体系结构

基本结构如下图:

image-20210915104636608.png

Throwable 是所有异常的父类,它有两个子类:ErrorException

Error :程序一旦出现 Error 错误,程序可能会停止运行。

Exception:与 Error 不同,程序中出现 Exception 异常有机会从问题中恢复,并尝试保持程序运行。

Java 检查异常和非检查异常

检查异常:所有不是 Runtime Exception 的异常,统称为 Checked Exception,又被称为检查性异常。这类异常的产生不是程序本身的问题,通常由外界因素造成的。为了预防这些异常产生时,造成程序的中断或得到不正确的结果,Java 要求编写可能产生这类异常的程序代码时,一定要去做异常的处理。

非检查异常: Java 语言将派生于 RuntimeException 类或 Error 类的所有异常称为非检查性异常。

image-20210915104205230.png

Java 中的异常处理

有两种方法可以处理抛出的异常:

  1. 在当前方法中通过 try-catch 的方式处理该异常。

  2. 在方法签名的后面通过 throws 重新抛出该异常。

Java 中的异常分类

我们可以将所有的异常分为三类:

  • 检查性异常(checked exceptions) 是必须在在方法的 throws 子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA希望你能够处理它们,因为它们以某种方式依赖于程序之外的外部因素。检查性异常表示在正常系统操作期间可能发生的预期问题,当你尝试通过网络或文件系统使用外部系统时,通常会发生这些异常。 大多数情况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。
  • 非检查性异常(unchecked Exceptions) 是不需要在throws子句中声明的异常。 由于程序错误,JVM并不会强制你处理它们,因为它们大多数是在运行时生成的。 它们扩展了 RuntimeException。 最常见的例子是 NullPointerException, 未经检查的异常可能不应该重试,正确的操作通常应该是什么都不做,并让它从你的方法和执行堆栈中出来。
  • 错误(errors) 是严重的运行时环境问题,肯定无法恢复。 例如 OutOfMemoryErrorLinkageErrorStackOverflowError,通常会让程序崩溃。

了解异常类的类型后,我们可能需要回答以下问题:

  • 异常情况有多糟糕以及异常的原因是什么?
  • 如何解决这个异常?
  • 我们需要重启JVM吗?
  • 我们需要重写代码吗?

知道异常类,我们可以预测可能出错的地方。考虑潜在的原因,我们可以假设问题的原因是什么以及如何解决它。在接下来的段落中,我们将回顾常见的异常并调查潜在的原因是什么。在我们的调查中,我们假设应用程序足够稳定并且已经完成开发和测试。

常见 Error 异常

image-20210915153410425.png
image.png

在大多数情况下,需要做的就是更改 JVM 配置或添加缺少的依赖项,仍然存在需要更改代码的情况,但它们不太可能在每种情况下更改。

常见 Runtime 异常

image-20210915154320891.png

Checked 和 Error 异常错误不会导致任何代码更改,但是在大多数情况下,运行时异常突出了代码中的真正问题,如果不重写代码就无法修复这些问题。

常见 Checked 异常

image-20210915155008329.png
image.png

如果我们查看最可能的原因,我们会发现其中的大多数 不仅不需要任何代码更改,甚至不需要重新启动应用程序。

Java 异常处理最佳实践

1. 不要忽略捕获的异常

Copycatch (NoSuchMethodException e) {
   return null;
}

虽然捕获了异常但是却没有做任何处理,除非你确信这个异常可以忽略,不然不应该这样做,这样会导致外面无法知晓该方法发生了错误,无法定位错误原因。

2. 在你的方法里抛出具体的检查性异常

Copypublic void foo() throws Exception { //错误方式
}

一定要避免出现上面的代码示例,它破坏了检查性异常的目的。 声明你的方法可能抛出的具体检查性异常,如果有太多这样的检查性异常,你应该把它们包装在你自己的异常中,并在异常消息中添加信息。 如果可能的话,你也可以考虑代码重构。

Copypublic void foo() throws SpecificException1, SpecificException2 { //正确方式
}

3. 捕获具体的子类而不是捕获 Exception 类

Copytry {
   someMethod();
} catch (Exception e) { //错误方式
   LOGGER.error("method has failed", e);
}

捕获 Exception 的问题是,如果稍后调用的方法为其方法声明添加了新的检查性异常,则开发人员的意图是应该处理具体的新异常,但是你的代码只是捕获 Exception (或 Throwable),那么永远不会知道这个新的异常,并且你的程序可能会在运行时的任何时候中断。

3. 永远不要捕获 Throwable 类

这是一个更严重的问题,因为 Error 也是 Throwable 的子类,Error 是 JVM 本身无法处理的不可逆转的错误,对于某些 JVM 的实现,JVM 可能实际上甚至不会在 Error 上调用 catch 子句。

4. 始终正确包装自定义异常中的异常,以便堆栈跟踪不会丢失

Copycatch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //错误方式
}

这破坏了原始异常的堆栈跟踪,并且始终是错误的,正确的做法是:

Copycatch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}

5. 要么记录异常要么抛出异常,但不要一起执行

Copycatch (NoSuchMethodException e) {  
    //错误方式 
   LOGGER.error("Some information", e);
   throw e;
}

正如上面的代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,并且对尝试分析日志的同事很不友好。

6. finally 块中永远不要抛出任何异常

Copytry {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
}

只要 cleanUp() 永远不会抛出任何异常,上面的代码没有问题,但是如果 someMethod() 抛出一个异常,并且在 finally 块中,cleanUp() 也抛出另一个异常,那么程序只会把第二个异常抛出来,原来的第一个异常(正确的原因)将永远丢失。如果在 finally 块中调用的代码可能会引发异常,请确保要么处理它,要么将其记录下来,永远不要让它从 finally 块中抛出来。

7. 始终只捕获实际可处理的异常

Copycatch (NoSuchMethodException e) {
   throw e; //避免这种情况,因为它没有任何帮助
}

这是最重要的概念,不要为了捕获异常而捕获,只有在想要处理异常时才捕获异常,或者希望在该异常中提供其他上下文信息。如果你不能在 catch 块中处理它,那么最好的建议就是不要只为了重新抛出它而捕获它。

8. 不要使用 printStackTrace() 语句或类似的方法

完成代码后,切勿忽略 printStackTrace(),最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何帮助,因为它不会附加任何上下文信息。

9. 对于不打算处理的异常,直接使用 finally

Copytry {
  someMethod();  //Method 2
} finally {
  cleanUp();    //do cleanup here
}

这是一个很好的做法,如果在你的方法中你正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,然后在 finally 块中进行清理,不要使用 catch 块。

10. 记住早 throw 晚 catch 原则

这可能是关于异常处理最著名的原则,简单说,应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。

这个原则隐含地说,你将更有可能把它放在低级方法中,在那里你将检查单个值是否为空或不适合。而且你会让异常堆栈跟踪上升好几个级别,直到达到足够的抽象级别才能处理问题。

11. 在异常处理后清理资源

如果你正在使用数据库连接或网络连接等资源,请确保关闭它们。如果你正在调用的 API 仅使用非检查性异常,则仍应使用 try-finally 块来清理资源。 在 try 模块里面访问资源,在 finally 里面最后关闭资源。即使在访问资源时发生任何异常,资源也会优雅地关闭。

12. 只抛出和方法相关的异常

相关性对于应用程序排查问题非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户任何相关的信息。相反,如果这种异常被包裹在自定义异常中,则会更好,NoSuchFileFoundException 则对该方法的用户更有用。

13. 切勿在程序中使用异常来进行流程控制

不要在项目中出现使用异常来处理应用程序逻辑,永远不要这样做,它会使代码很难阅读和理解。

14. 尽早验证用户输入以在请求处理的早期捕获异常

始终要在非常早的阶段验证用户输入,甚至在达到 controller 之前,它将帮助你把核心应用程序逻辑中的异常处理代码量降到最低。如果用户输入出现错误,还可以保证与应用程序一致。

例如:如果在用户注册应用程序中,遵循以下逻辑:

  1. 验证用户
  2. 插入用户
  3. 验证地址
  4. 插入地址
  5. 如果出问题回滚一切

这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后将用户数据置于 dao 层并进行数据库更新。正确的做法是:

  1. 验证用户
  2. 验证地址
  3. 插入用户
  4. 插入地址
  5. 如果问题回滚一切

15. 一个异常只能包含在一个日志中

CopyLOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");

不要像上面这样做,对多个 LOGGER.debug() 调用使用多行日志消息可能在你的测试用例中看起来不错,但是当它在具有 100 个并行运行的线程的应用程序服务器的日志文件中显示时,所有信息都输出到相同的日志文件,即使它们在实际代码中为前后行,但是在日志文件中这两个日志消息可能会间隔 100 多行。应该这样做:

CopyLOGGER.debug("Using cache sector A, using retry sector B");

16. 将所有相关信息尽可能地传递给异常

有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?

17. 终止掉被中断的线程

Copywhile (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {} //别这样做
  doSomethingCool();
}

InterruptedException 异常提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。

应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。修改后的程序如下:

Copywhile (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();

18. 对于重复的 try-catch,使用模板方法

在代码中有许多类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。

例如,在尝试关闭数据库连接时的异常处理。

Copyclass DBUtil{
    public static void closeConnection(Connection conn){
        try{
            conn.close();
        } catch(Exception ex){
            //Log Exception - Cannot close connection
        }
    }
}

这类的方法将在应用程序很多地方使用,不要把这块代码放的到处都是,而是定义上面的方法,然后像下面这样使用它:

Copypublic void dataAccessCode() {
    Connection conn = null;
    try{
        conn = getConnection();
        ....
    } finally{
        DBUtil.closeConnection(conn);
    }
}

19. 使用 JavaDoc 中记录应用程序中的所有异常

把用 JavaDoc 记录运行时可能抛出的所有异常作为一种习惯,其中也尽量包括用户应该遵循的操作,以防这些异常发生。

20. 使用 try-with-resource 自动地关闭资源

1、当一个外部资源的句柄对象实现了 AutoCloseable 接口,JDK7中便可以利用 try-with-resource 语法更优雅的关闭资源,消除板式代码。

2、使用 try-with-resource 时,如果对外部资源的处理和对外部资源的关闭均遭遇了异常,“关闭异常”将被抑制,“处理异常”将被抛出,但“关闭异常”并没有丢失,而是存放在“处理异常”的被抑制的异常列表中。

public static void main(String[] args) {
    try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
        System.out.println(inputStream.read());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

将外部资源的句柄对象的创建放在try关键字后面的括号中,当这个try-catch代码块执行完毕后,Java会确保外部资源的close方法被调用。

总结

这篇文章首先介绍了什么是异常,以及异常的三种分类,然后通过 20 个最佳实践来讨论如何处理异常,希望能在以后异常处理的时候有所改进及感悟。

参考文档:

https://dzone.com/articles/java-exceptions-1

http://www.yinwang.org/blog-cn/2015/11/21/programming-philosophy

https://www.cnblogs.com/wupeixuan/p/11746117.html

https://howtodoinjava.com/best-practices/java-exception-handling-best-practices/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342