异常至少会被捕获一次,且只会被捕获一次
1.异常至少会被捕获一次
如果一个方法没有捕获异常,那么异常将会继续向上抛出,直到被捕获。
如果一直没有被程序捕获,将会被jvm捕获。
2.异常只会被捕获一次
异常只会被捕获一次,也就是说,只会被最近的捕获代码捕获一次。后面的捕获代码,不再捕获已经被捕获的异常。
如何抛出异常?
分类
1.显式抛出
2.隐式抛出
显式抛出
分类
1.代码里面抛出
2.方法签名抛出
真正抛出的还是代码里抛出,任何一个异常的本质都是在代码里的某个地方throw e。方法签名只是申明。
方法签名的作用,只是为了告诉调用者可能抛出什么异常。调用者通过方法签名就知道要处理什么异常,而不是进到方法里去看到底抛出什么异常。另外,编译器也会校验方法签名,自动提示程序员需要处理什么异常。
建议使用方法签名,特别是自定义异常。
隐式抛出
如果方法签名不抛出异常,或者方法的代码和签名都没有抛出异常,该异常仍然会继续向上抛出,直到被程序或者jvm捕获。
idea会提示方法签名必须要抛出异常,否则运行会报错。

注意
1.异常的时候,抛出异常,当前方法就退出了,不会执行return语句,也没有返回值。
2.如果捕获之后,只是打印日志,没有抛出异常,那么try代码块里面异常之后的代码不会执行,但是catch和catch之后的代码仍然还是会执行。
public static void main(String[] args) {
try {
m1();
log.info("不执行");
} catch (Exception e) {
log.error("",e);
}
//捕获异常之后,继续执行后面的代码
log.info("执行");
}

3.finally代码块
设计的时候有两个目的,
1)不管成功(执行return语句之前)还是失败(执行throw语句之前),都要先执行finally代码块释放对象,然后再执行return语句或throw语句。
2)避免在两个地方写释放对象的重复代码,两个地方是try和catch代码块。有了finally代码块,就只需要写一次。
catch代码块
catch代码块,跟方法差不多,有入参。
public static void main(String[] args) {
try {
m1();
log.info("不执行");
} catch (Exception e) { //e是入参
log.error("",e);
}
//捕获异常之后,继续执行后面的代码
log.info("执行");
}
还有synchronized代码块,也是一样,有入参。
public static void main(String[] args) {
try {
m1();
log.info("不执行");
} catch (Exception e) {
log.error("",e);
}
//捕获异常之后,继续执行后面的代码
log.info("执行");
synchronized (a){ //a是入参
...
}
}
方法签名
抛出多个异常
如果代码抛出多个异常,方法签名也申明多个异常。
为什么方法签名要声明?
因为有可能不开源,只是一个jar。如果没有方法签名,就不知道要处理什么异常。
调用远程服务
1.客户端
必须打印入参、捕获异常/打印报错方法调用栈/抛出异常、打印响应数据。
2.服务器
返回数据尽量使用结果对象,而不是抛出异常。因为结果对象包含状态和描述信息,客户端可以通过校验状态来更好的处理不同的业务逻辑。
避免重复打印报错日志
为什么要避免重复处理异常,因为要避免重复打印日志。既然不需要打印报错日志,就没有必要重复处理异常(重复捕获/重复打印报错日志/重复抛出异常)。
还有,
1.每个服务,都有相同的日志,不知道到底是哪个服务报错了。
2.日志中心中间件elk,搜索到多个重复的日志。
具体来说
controller必须捕获异常和打印报错方法调用栈,service/dao如果是同样的异常并且不是远程服务,则可以不需要重复打印日志、捕获异常和抛出异常。因为重复捕获异常之后,打印出来也是重复的方法调用栈。相同的异常,只需要捕获和打印一次,通过原始异常的方法调用栈,就可以定位到报错代码行。
如果是util类或其他的类,是和controller/service/dao不一样的异常,则必须捕获异常/打印错误方法调用栈日志/抛出异常。
总结
1.相同的异常,只需要处理异常。不需要重复捕获一次/打印日志/方法签名申明。
2.不同的异常,必须处理异常。
3.调用远程方法,必须处理异常。
父子异常
如果抛出多个异常,那么方法签名申明多个异常。
如果有多个异常,且是父子异常。那么
1.顺序
先子后父。即越具体的异常,越放到前面。否则,编译器会报错。
2.方法签名
也是一样,越具体的就放前面。
代码

异常尽量越具体越好
如果不具体,也没有关系,仍然会被捕获。
比如,抛出具体异常,但是捕获的时候却是父异常Exception,这个时候仍然会捕获异常,基本上没有什么不同。唯一的不同就是跟方法签名显式的申明一样,如果显式的捕获具体异常而不是父异常,那么程序员一眼就知道是什么错误,否则的话,还需要看具体的报错日志才知道是什么异常。
代码

如果服务器报错,是抛出异常还是返回响应数据给客户端?
如果客户端不需要打印报错日志,那么服务器就不应该抛出异常,而是返回响应数据。比如:
1.远程方法调用 //公司内部的远程方法调用
2.controller //浏览器客户端,只需要看到用户友好的报错信息,不需要看方法调用栈。
3.访问外部公司api //比如支付宝支付接口
以上情况都应该返回响应数据,即result对象。result对象包含1.状态 2.描述信息。客户端根据结果对象的状态字段,处理业务逻辑。
当然,以上情况都必须要捕获异常,只是不应该抛出异常。客户端收到的应该是用户良好的结果对象,而不是异常。
异常分类
1.需要捕获的异常
2.不需要捕获的异常
需要捕获的异常
都是外部原因,比如文件不存在。等文件存在了,下次操作就没有异常。所以,异常的时候,就要捕获处理,然后提示:文件不存在。
还有比如,入参为空,也是客户端请求的时候缺少参数。客户端看到提示缺少参数,下次就会填写参数,然后就没有异常了。
总结,异常之所以需要捕获,是因为你要处理它。如果不处理,就没有必要捕获。
不需要捕获的异常
1.运行时异常
2.jvm异常
除此之外,其他都是需要捕获的异常。
运行时异常,是程序错误。比如,除数为0,数组索引越界,空指针,这些异常都是代码有Bug。你要做的是修改bug,而不是捕获异常,因为你不捕获异常,这个异常还是会抛出来,所以正确的做法应该是写没有bug的代码,万一写了bug,也应该让它抛出来,根据报错方法调用栈找到bug,而不是捕获异常。因为你捕获了异常,也只是打印了错误日志,但是你不捕获,也一样会打印错误日志。所以没有必要捕获运行时异常。
jvm异常,是jvm错误。正因为是jvm错误,所以更不应该去捕获,你说你捕获了之后,能干嘛?比如,堆内存溢出或者栈内存溢出,你捕获了之后,啥也干不了,除了打印日志。所以还不如让jvm抛出异常。
总结
1.不要捕获Throwable //因为包含了jvm异常和运行时异常
2.不要捕获RuntimeException //因为运行时异常说明代码有bug,你就应该让它直接抛出来。当看到抛出错误的时候,立即修改bug。
异常类继承图

异常也是对象,所以抛出的时候,
1.throw e; //e是对象
2.throw new Exception("描述信息",e); //要么就new 一个对象
硬件故障
机器都挂了,如果不捕获,那怎么告警?硬件故障应该不是通过报错日志。而是使用专门的硬件故障监控软件,比如zabbix。
jvm也是同理,应该使用专门的监控jvm的软件。比如,内存不够保存的时候生成dump文件。然后,基于dump文件分析jvm故障。
不要嵌套抛出异常
不要嵌套抛出异常,因为嵌套抛出,会被当前方法自己捕获,然后又再抛出一次,那么就打印了两次相同的报错日志。
代码
public void m4() throws Exception {
try{
//不要嵌套抛出异常
try{
}catch (Exception e){
throw e; //嵌套抛出异常
}
if (false) {
throw new Exception("描述信息"); //根据响应数据的状态字段,嵌套抛出异常
}
}catch (Exception e){
throw e;
}
}
历史
以前的编程语言,但是依靠01或true/false来处理异常(类似java语言里使用结果对象Result来处理异常)。
这个不是语言自带的,而是程序员自己定义的,约定的。
只有Java内置了异常处理机制。
微服务
但是基于微服务的架构,还是应该使用结果对象,而不是抛出异常。
1.如果是服务内部,则抛出异常。
2.如果是外部服务,还是使用结果对象比较好。
参考
java官方文档
head first java