1.异常:这种情况下的异常,可以通过完善任务重试机制,当执行异常时,保存当前任务信息加入重试队列。重试的策略根据业务需要决定,当达到重试上限依然无法成功,记录任务执行失败,同时发出告警。
2.日志:类比消息中间件,处在不同线程之间的同一任务,简单高效一点的做法可能是用traceId/requestId串联。有些日志系统本身支持MDC/NDC功能,可以串联相关联的日志。
(需深入学习理解)
image.png- Throwable:它是异常处理机制的基本组成类型,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch)。
- Exception 和 Error 都是继承了 Throwable 类。
Exception 和 Error 体现了 Java 平台设设计者对不同异常情况的分类。
- Error:系统错误,虚拟机出错,我们处理不了,也不需要我们来处理。比如OutOfMeoryError。
- Exception:可以捕获的异常,且作出处理。也就是要么捕获异常并作出处理,要么继续抛出异常。
Exception又分为检查型异常和非检查型异常
- 检查型异常:在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。如FileNotFoundException客户端需要知道是文件没有找到的问题,客户端可以通过其它方法来解决这个问题如更换其它路径等。
- 非检查型异常:就是所谓的运行时异常,类似 NullPointerException,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
第一,理解 Throwable、Exception、Error 的设计和分类。
掌握那些应用最为广泛的子类
比如 NoClassDefFoundError 和ClassNotFoundException 有什么区别,这也是个经典的入门题目。
常见异常:
- ActivityNotFoundException:该异常是当调用了 startActivity() 之后,找不到匹配的 Activity 时抛出该异常。
- BadTokenException:当添加一个新的 window 时,如果 LayoutParams 不合法,就会抛出该异常。
- ClassCastException:父类可以通过强制类型转换成具体某个子类,但如果强转的两个类之间不存在继承关系,那么就会抛出该异常。
- ConcurrentModificationException:同时修改异常
- IndexOutOfBoundsException:数组越界异常
- NullPointerException:空指针异常
- IOException:IO 异常,属于检查型异常,必须通过 try catch 代码块捕获才能通过编译阶段。
- OutOfMemoryError:内存溢出错误,这类问题属于 Error。
- StackOverflowError:这类错误很严重,表示程序陷入了死循环当中。
- NoClassDefFoundError:通常出现的场景是:编译阶段没问题,但程序运行期间却出现该问题。原因一般是由于打包时,jar 出现问题,部分类没有打包进去,导致的问题。
- ClassNotFoundException:这个异常,同样属于相关类找不到的问题,但出现的场景通常是由于程序中使用了反射,或者动态加载之类的方式,使用了错误的类名,导致的问题。还有可能是由于混淆导致。
- NumberFormatException:数字转换格式异常。
第二,理解 Java 语言中操作 Throwable 的元素和实践。
try-catch-finally:捕获异常
try (BufferedReader br = new BufferedReader(…);
BufferedWriter writer = new BufferedWriter(…)) {
// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch
// Handle it
} finally{
// do something
}
throw 是语句抛出一个异常。
public class ThrowTest {
public static void main(String[] args) {
try{
throwChecked(3);
}catch(Exception e) {
System.out.println(e.getMessage());
}
throwRuntime(-3);
}
//该方法内抛出一个Exception异常对象,必须捕获或抛给调用者
public static void throwChecked(int a) throws Exception {
if(a < 0) {
throw new Exception("a的值应大于0,不符合要求")
}
}
//该方法内抛出一个RuntimeException对象,可以不理会直接交给JVM处理
public static void throwRuntime(int a) {
if(a < 0) {
throw new RuntimeException("a的值应大于0,不符合要求")
}
}
}
- throw语句用在方法体内,表示抛出异常,由方法体内的语句处理
- throw是具体向外抛异常的动作,所以它是抛出一个异常实例。
- throw要么和try-catch-finally语句配套使用,要么与throws配套使用
- 如果抛出的是RuntimeException则既可以显示使用try…catch捕获也可以不理会它
throws 是方法可能抛出异常的声明。
- 用在声明方法时,表示该方法可能要抛出异常
- 给调用者处理或者交给JVM。JVM对异常的处理方式是:打印异常的跟踪栈信息并终止程序运行。
- throws可以声明多个异常,用逗号隔开;
- 调用者必须做出处理(捕获或继续抛出)。否则编译是不会通过的。
注意:
方法覆盖的时候,如果子类覆盖了父类的方法,子类的方法不能声明抛出比父类更多的异常类型。如果声明的比父类更多的异常类型,编译器是通不过的。
遵循“Throw early catch late”原则
简单来说就是:在异常出现时就将其抛出,抓取应该在能够获取足够信息的时候。简单来说,底层的方法应该更多的抛出异常,异常应该更多的在顶层代码中抓取处理。
第三,异常处理的两大基本原则
- 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
- 不要生吞(swallow)异常。
为什么要尽量捕获特定异常?
- 软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息。
- Exception无法提供具体的异常信息,不利于处理;强制客户端处理(抓取)一些不需要关注的异常。
什么是生吞异常?
- 生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,直接try catch掉,不做任何处理,不把异常抛出来,或者也没有输出到日志(Logger)之类。
- 存在的隐患是:程序可能在后续代码以不可控的方式结束。开发人员就很难找到问题所在,什么原因导致的异常。
第四,自定义异常
除了保证提供足够的信息,还有两点需要考虑:
- 是否需要定义成 Checked Exception
- 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。
public class AuctionException extends Exception {
//无参构造
public AuctionException() {}
//含参构造
//通过调用父类的构造器将字符串msg传给异常对象的massage属性,
//massage属性就是对异常的描述
public AuctionException(String msg) {
super(msg);
}
}
第五,性能分析
- try-catch 代码段会产生额外的性能开销
- Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。
或者换个角度说,它往往会影响 JVM 对代码进行优化。
建议
-
不要过度使用异常:对于完全已知的错误应编写处理这种错误代码从而提高代码的健壮性,只有外部的、不能确定的和不可预知的运行时错误使是用异常,并且异常机制的效率低于正常的流程控制。
-
仅捕获有必要的代码段,尽量不要一个大的 try包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的 Exception 也是一种思路。
心得感悟(摘抄评论区)
1 不要推诿或延迟处理异常,就地解决最好,并且需要实实在在的进行处理,而不是只捕捉,不动作。
2 一个函数尽管抛出了多个异常,但是只有一个异常可被传播到调用端。最后被抛出的异常时唯一被调用端接收的异常,其他异常都会被吞没掩盖。如果调用端要知道造成失败的最初原因,程序之中就绝不能掩盖任何异常。
3 不要在finally代码块中处理返回值。
4 按照我们程序员的惯性认知:当遇到return语句的时候,执行函数会立刻返回。但是,在Java语言中,如果存在finally就会有例外。除了return语句,try代码块中的break或continue语句也可能使控制权进入finally代码块。
5 请勿在try代码块中调用return、break或continue语句。万一无法避免,一定要确保finally的存在不会改变函数的返回值。
6 函数返回值有两种类型:值类型与对象引用。对于对象引用,要特别小心,如果在finally代码块中对函数返回的对象成员属性进行了修改,即使不在finally块中显式调用return语句,这个修改也会作用于返回值上。
7 勿将异常用于控制流。
8 如无必要,勿用异常。
网友评论