本期话题:聊聊Java的异常吧,Exception和Error?运行时异常与一般异常?
General Knowledge or Common Sense
- Exception和Error继承了Throwable,可以被throw或catch,是异常的处理机制的基本组成类型
- Exception是程序正常运行中,可以预料的意外,应当被捕获并进行处理
- Error是非正常情况,无需捕获和处理,比如OutOfMemoryError等,由JVM抛出和处理
- Exception分类
- 必检异常[checked]:编译期会检查,必须显示的捕获和处理
- 非必检异常[unchecked]:运行时异常,通常通过编码避免逻辑错误
Throwable、Exception、Error的设计和分类 + 自定义异常
小作业:NoClassDefFoundError vs ClassNotFoundException
- NoClassDefFoundError:
JVM或者ClassLoader在加载一个class(通过方法调用或者创建实例)的时候,目标class没有被找到。编译期间可以被编译,但是运行时找不到。可能的原因,比如OSGI文件里的lib包,没有在导出插件时被一起导出。
/**
* Thrown if the Java Virtual Machine or a <code>ClassLoader</code> instance
* tries to load in the definition of a class (as part of a normal method call
* or as part of creating a new instance using the <code>new</code> expression)
* and no definition of the class could be found.
* <p>
* The searched-for class definition existed when the currently
* executing class was compiled, but the definition can no longer be
* found.
*
* @author unascribed
* @since JDK1.0
*/
- ClassNotFoundException
通过Class.ForName或者ClassLoader.findSystemClass、CLassLoader.loadClass通过类的QualifiedName加载失败,常见于XML文件注册注入类对象时发生
/**
* Thrown when an application tries to load in a class through its
* string name using:
* <ul>
* <li>The <code>forName</code> method in class <code>Class</code>.
* <li>The <code>findSystemClass</code> method in class
* <code>ClassLoader</code> .
* <li>The <code>loadClass</code> method in class <code>ClassLoader</code>.
* </ul>
* <p>
* but no definition for the class with the specified name could be found.
*
* <p>As of release 1.4, this exception has been retrofitted to conform to
* the general purpose exception-chaining mechanism. The "optional exception
* that was raised while loading the class" that may be provided at
* construction time and accessed via the {@link #getException()} method is
* now known as the <i>cause</i>, and may be accessed via the {@link
* Throwable#getCause()} method, as well as the aforementioned "legacy method."
*
* @author unascribed
* @see java.lang.Class#forName(java.lang.String)
* @see java.lang.ClassLoader#findSystemClass(java.lang.String)
* @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
* @since JDK1.0
*/
How to use code handle Exception?
- try-catch-finally
- try-with-resources
- multiple catch
//扩展了AotuCloseable或者Closeable的对象
try (BufferedReader br = new BufferedReader(…);
BufferedWriter writer = new BufferedWriter(…)) {// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch
// Handle it
}
Something important
-
捕获特定异常,避免直接捕获Exception
-
除非你非常确定,否则不要捕获Throwable或者Error
-
不要生吞(swallow)异常,异常可以帮助诊断问题
-
避免直接使用e.printStackTrace()
- printStackTrace输出位置不一定:API描述"Print this throwable and its backtrace to the standard error stream",也是就是可以通过指定System.err,重定向输出流的位置,如果输入到linux的black hole…嗯…启动命令类似如下:
nohup ./app.sh >/dev/null 2>&1 &
关于Black Hole可以看下Null Device 介绍
- 在printStackTrace没有重定向的时候,输出到console是非常低效的,同步IO操作会导致线程卡死
-
throw early
public void readPreferences(String filename) { //提前校验非空的内容,减少异常的堆栈 Objects. requireNonNull(filename); //...perform other operations... InputStream in = new FileInputStream(filename); //...read the preferences file... }
-
catch late
调用方或者更高的层面有清晰的(业务)逻辑,处理异常更加适合,因此底层代码可以继续向上抛出异常 -
自定义异常
- 是否设置为Checked Exception:必检异常是为了把程序从异常情况恢复,除此之外使用RuntimeException及其子类,避免代码出现太多的try-catch块
- wrap msg:通过自定义异常,将exception的msg封装为可读,脱敏的自然语言,真正的异常记录日志,提高程序的安全性,避免暴露细节
性能为王
-
try-catch代码会产生额外性能开销,影响JVM对代码的优化,因此需要避免一个大的try包住大段代码;
-
避免使用try-catch的异常控制代码流程
//这是一个非常典型的反例,也是一个误区. /** * 处理业务消息 * @param message 要处理的消息 */ public void processMessage(Message<String> message) { try{ // 处理消息验证 // 处理消息解析 // 处理消息入库 }catch(ValidateException e ){ // 验证失败 }catch(ParseException e ){ // 解析失败 }catch(PersistException e ){ // 入库失败 } } //修改后的逻辑 /** * 处理业务消息 * @param message 要处理的消息 */ public void processMessage(Message<String> message) { // 处理消息验证 if(!message.isValud()){ MessageLogService.log("消息校验失败"+message.errors()) return ; } // 处理消息解析 if(!message.parse()){ MessageLogService.log("消息解析失败"+message.errors()) return ; } // TODO .... }
-
Java每实例化一个Exception都会对当时的栈进行快照,如果发生的异常非常频繁,性能会有损耗。当服务的吞吐下降时,检查发生最频繁的Exception也是一种思路
延展小话题:异常的日志
分布式环境下出现异常后,如果快速定位问题呢。一般通过ELK可以搭建日志平台,收集日志。增加traceid/requestId也可以串联
多线程日志:通过MDC/NDC功能串联日志,或者traceid和requestid
抛一个问题:Checked Exception对Lambda/Stream造成的麻烦(目前体会尚欠,后续更新)
lambda的程序,调用一个抛出检查异常的方法,现在语言层面并没有流畅处理机制,往往需要自定义functional interface
再抛一个问题:Reactive Stream对异常处理的最佳实践(目前体会尚欠,后续更新)
1.异常:这种情况下的异常,可以通过完善任务重试机制,当执行异常时,保存当前任务信息加入重试队列。重试的策略根据业务需要决定,当达到重试上限依然无法成功,记录任务执行失败,同时发出告警。
2.日志:类比消息中间件,处在不同线程之间的同一任务,简单高效一点的做法可能是用traceId/requestId串联。有些日志系统本身支持MDC/NDC功能,可以串联相关联的日志。
全局异常Spring MVC的方式就很实用;
Checked Exception 不兼容 functional 编程,如果你写过 Lambda/Stream 代码,相信深有体会。这段话可以详细剖析下吗?