Java 异常分析
Java 异常分析
本文是对以下内容的分析:
Java异常设计
Java 异常分类
Java异常可以告诉什么问题
Java异常处理最佳实践
Java Exception 是为了处理应用程序的异常行为而创建的类。在本文中,将解释如何使用 Java Exception 类以及如何在考虑现有 Java Exception 设计的情况下创建异常结构。Java 异常概念是 Java 中的重要里程碑之一,每个开发人员都必须知道它。
Java 异常体系结构
基本结构如下图:
Throwable
是所有异常的父类,它有两个子类:Error
和 Exception
。
Error :程序一旦出现 Error 错误,程序可能会停止运行。
Exception:与 Error 不同,程序中出现 Exception 异常有机会从问题中恢复,并尝试保持程序运行。
Java 检查异常和非检查异常
检查异常:所有不是 Runtime Exception
的异常,统称为 Checked Exception
,又被称为检查性异常。这类异常的产生不是程序本身的问题,通常由外界因素造成的。为了预防这些异常产生时,造成程序的中断或得到不正确的结果,Java 要求编写可能产生这类异常的程序代码时,一定要去做异常的处理。
非检查异常: Java 语言将派生于 RuntimeException
类或 Error
类的所有异常称为非检查性异常。
Java 中的异常处理
有两种方法可以处理抛出的异常:
在当前方法中通过
try-catch
的方式处理该异常。在方法签名的后面通过
throws
重新抛出该异常。
Java 中的异常分类
我们可以将所有的异常分为三类:
- 检查性异常(checked exceptions) 是必须在在方法的 throws 子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA希望你能够处理它们,因为它们以某种方式依赖于程序之外的外部因素。检查的异常表示在正常系统操作期间可能发生的预期问题。 当你尝试通过网络或文件系统使用外部系统时,通常会发生这些异常。 大多数情况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。
- 非检查性异常(unchecked Exceptions) 是不需要在throws子句中声明的异常。 由于程序错误,JVM并不会强制你处理它们,因为它们大多数是在运行时生成的。 它们扩展了 RuntimeException。 最常见的例子是 NullPointerException, 未经检查的异常可能不应该重试,正确的操作通常应该是什么都不做,并让它从你的方法和执行堆栈中出来。
- 错误(errors) 是严重的运行时环境问题,肯定无法恢复。 例如
OutOfMemoryError
,LinkageError
和StackOverflowError
,通常会让程序崩溃。
了解异常类的类型后,我们可能需要回答以下问题:
- 异常情况有多糟糕以及异常的原因是什么?
- 如何解决这个异常?
- 我们需要重启JVM吗?
- 我们需要重写代码吗?
知道异常类,我们可以预测可能出错的地方。考虑潜在的原因,我们可以假设问题的原因是什么以及如何解决它。在接下来的段落中,我们将回顾常见的异常并调查潜在的原因是什么。在我们的调查中,我们假设应用程序足够稳定并且已经完成开发和测试。
常见 Error 异常
异常类型 | 潜在原因 | 原因的可能性有多大 | 怎么修复 | 需要重写代码吗? | 需要重启JVM吗? |
OutOfMemoryError | 应用程序吃掉了所有内存 | 高的 | 增加堆内存大小 | 不 | 是的 |
内存泄漏 | 低的 | 查找内存泄漏并修复 | 是的 | 是的 | |
StackOverflowError | 堆栈内存不足 | 高的 | 增加堆栈内存大小 | 不 | 是的 |
无限递归 | 低的 | 设置递归调用的限制 | 是的 | 是的 | |
NoClassDefFoundError | 缺少依赖 | 高的 | 添加依赖或修复依赖配置 | 不 | 是的 |
初始化期间加载类失败 | 低的 | 更改初始化过程 | 是的 | 是的 |
在大多数情况下,需要做的就是更改 JVM 配置或添加缺少的依赖项,仍然存在需要更改代码的情况,但它们不太可能在每种情况下更改。
常见 Runtime 异常
异常类型 | 潜在原因 | 原因的可能性有多大 | 怎么修复 | 需要重写代码吗? | 需要重启吗? |
NullPointerException | 预期的不可为空的对象为空 | 高的 | 调用前添加验证层 | 是的 | 是的 |
某些资源不可用并返回空数据 | 中等的 | 调用前添加验证层 | 是的 | 是的 | |
ConcurrentModificationException | 迭代期间集合已更改 | 高的 | 分别进行集合迭代和修改 | 是的 | 是的 |
集合在迭代期间已从另一个线程更改 | 高的 | 为集合添加同步 | 是的 | 是的 | |
IlliegalArgumentException | 传递的参数无效 | 高的 | 在传递参数之前添加验证 | 是的 | 是的 |
NumberFormatException | 传递的参数格式错误或符号错误 | 高的 | 在传递数据之前添加格式或删除不可见符号 | 是的 | 是的 |
ArrayIndexOutOfBoundsException | 指令试图通过不存在的索引访问单元格 | 高的 | 将访问逻辑更改为正确的逻辑 | 是的 | 是的 |
NoSuchElementException | 当指针已经改变位置时访问元素 | 高的 | 将访问逻辑更改为正确的逻辑 | 是的 | 是的 |
集合在迭代过程中被修改 | 高的 | 为集合添加同步 | 是的 | 是的 |
Checked 和 Error 异常错误不会导致任何代码更改,但是在大多数情况下,运行时异常突出了代码中的真正问题,如果不重写代码就无法修复这些问题。
常见 Checked 异常
异常类型 | 潜在原因 | 原因的可能性有多大 | 怎么修复 | 需要重写代码吗? | 需要重启吗? |
FileNotFoundException | 该文件不存在 | 高的 | 创建文件 | 不 | 不 |
应用程序调用错误的路径 | 低的 | 修复错误的路径生成 | 是的 | 是的 | |
IOException | 访问资源无效 | 高的 | 让资源再次可用 | 不 | 不 |
ClassNotFoundException | 该类未添加依赖项 | 高的 | 添加缺少的依赖项 | 不 | 是的 |
实现调用了错误的类 | 中等的 | 更改类调用 | 是的 | 是的 | |
SqlException | 架构与查询不匹配 | 高的 | 将缺失的脚本应用到数据库 | 不 | 不 |
查询错误 | 低的 | 更改查询 | 是的 | 是的 | |
拒绝连接 | 高的 | 打开数据库,更改端口 | 不 | 不 | |
InterruptedException | 依赖线程通知中断(锁释放,另一个线程完成操作) | 高的 | 没有必要修复它;这是一种通知相关线程中事件的方法 | 不 | 不 |
另一个线程中断并使用中断通知相关 | 中等的 | 修复另一个线程中出现的问题(可以是任何东西) | 是的 | 是的 | |
SocketException | 端口被占用 | 高的 | 打开/释放端口 | 不 | 不 |
服务器断开连接 | 高的 | 检查网络连接 | 不 | 不 |
如果我们查看最可能的原因,我们会发现其中的大多数 不仅不需要任何代码更改,甚至不需要重新启动应用程序。
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
之前,它将帮助你把核心应用程序逻辑中的异常处理代码量降到最低。如果用户输入出现错误,还可以保证与应用程序一致。
例如:如果在用户注册应用程序中,遵循以下逻辑:
- 验证用户
- 插入用户
- 验证地址
- 插入地址
- 如果出问题回滚一切
这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后将用户数据置于 dao
层并进行数据库更新。正确的做法是:
- 验证用户
- 验证地址
- 插入用户
- 插入地址
- 如果问题回滚一切
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/
Java 异常分析的更多相关文章
- java异常分析;剖析printStackTrace和fillInStackTrace
Java异常的栈轨迹(Stack Trace) 捕获到异常时,往往需要进行一些处理.比较简单直接的方式就是打印异常栈轨迹Stack Trace.说起栈轨迹,可能很多人和我一样,第一反应就是printS ...
- java.net.SocketException:Software caused connection abort: recv failed 异常分析 +socket客户端&服务端代码
java.net.SocketException:Software caused connection abort: recv failed 异常分析 分类: 很多的技术 2012-01-04 12: ...
- Java ConcurrentModificationException 异常分析与解决方案
Java ConcurrentModificationException 异常分析与解决方案http://www.2cto.com/kf/201403/286536.html java.util.Co ...
- JAVA异常处理原则和log4j输出详细异常分析
1.多用try,catch;不要一个try,catch包含所有内容 好处:不同模块抓取不同异常,某一模块异常挂了,不影响其他模块的程序的进行 2.多写几个catche:尽量不要使用Exception这 ...
- 浅谈java异常[Exception]
学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:589809992 我们一起学Java! 一. 异常的定义 在<java编程思想 ...
- Java 性能分析工具 , 第 3 部分: Java Mission Control
引言 本文为 Java 性能分析工具系列文章第三篇,这里将介绍如何使用 Java 任务控制器 Java Mission Control 深入分析 Java 应用程序的性能,为程序开发人员在使用 Jav ...
- 第11章 Java异常与异常处理
1.Java异常简介 1.什么是异常异常出现的时候代码会无法正常运行下去,会产生各种问题2.捕捉异常的作用提早发现异常,方便查找问题,并给出解决方法3.Java中的异常1.Java中所有不正常的类都是 ...
- java异常架构图 和几个面试题
1.java异常架构图 粉红色的是受检查的异常(checked exceptions),其必须被 try{}catch语句块所捕获,或者在方法签名里通过throws子句声明.受检查的异常必须在编译时被 ...
- Java 异常讲解(转)
六种异常处理的陋习 你觉得自己是一个Java专家吗?是否肯定自己已经全面掌握了Java的异常处理机制?在下面这段代码中,你能够迅速找出异常处理的六个问题吗? 1 OutputStreamWrite ...
随机推荐
- [ZJCTF 2019]Login
学了一段时间的堆溢出现在继续做题, 例行检查一下 64位的程序放入ida中 shift+f12查看程序函数 可以看到非常明显的后门程序 查看主函数 发现了程序给的账号和密码,但是没有看到明显的栈溢出漏 ...
- Windows服务(.net Core 3.1-Topshelf-log4net-quartz)
https://github.com/yezei/Topshelf-log4net-quartz.git
- matplotlib模块详解
简单绘图,折线图,并保存为图片 import matplotlib.pyplot as plt x=[1,2,3,4,5] y=[10,5,15,10,20] plt.plot(x,y,'ro-',c ...
- CF1428A Box is Pull 题解
Content 有一个兔子拖着一个盒子在走,每秒钟可以带着盒子走一个单位,也可以不带着盒子走一个单位.当且仅当兔子和盒子的距离不超过 \(1\) 时可以带着盒子走一个单位.现给出 \(t\) 次询问, ...
- netty系列之:小白福利!手把手教你做一个简单的代理服务器
目录 简介 代理和反向代理 netty实现代理的原理 实战 总结 简介 爱因斯坦说过:所有的伟大,都产生于简单的细节中.netty为我们提供了如此强大的eventloop.channel通过对这些简单 ...
- [Elasticsearch] ES 的Mapping 设计在实际场景中应用
背景 项目中有个需求是需要几个字段作为标签,统计各个标签的文档数量,同时支持分词后的全文检索功能. 原有的mapping设计: curl -XPUT http://ip:9200/meta_es_me ...
- SpringBoot内嵌ftp服务
引入依赖 <!-- https://mvnrepository.com/artifact/org.apache.ftpserver/ftpserver-core --> <depen ...
- Spring Tool Suite(STS)基本安装配置
下载jar包:https://spring.io/tools 下载完成后 打开所在文件夹 执行命令行 jar -jar 下载的jar包文件名 然后会自动解压 双击打开软件 配置主面板窗口 配置好之后 ...
- SpringBoot 整合Spring Security框架
引入maven依赖 <!-- 放入spring security依赖 --> <dependency> <groupId>org.springframework.b ...
- 【九度OJ】题目1153:括号匹配问题 解题报告
[九度OJ]题目1153:括号匹配问题 解题报告 标签(空格分隔): 九度OJ http://ac.jobdu.com/problem.php?pid=1153 题目描述: 在某个字符串(长度不超过1 ...