1. 异常处理
意义
try-catch-finally在开发中只是将可能出现异常的一段代码包起来,当其出现异常时是以一种友好的方式体现出来,并不能真正解决代码上的错误,异常的情况下还是需要修改代码
try-catch-finally需要和 编译时异常 绑定在一起,因为编译时异常无法运行
体系结构
java.lang.Throwable
|-----java.lang.Error:一般不编写针对性的代码进行处理。一般爆error错误,一般都是自己的代码写的有问题。
|-----java.lang.Exception:可以进行异常的处理
|------编译时异常(checked),指的是这一块可能会发生异常,为了代码的健壮性,需要加入预处理的代码,一旦真的出错了,就去执行预处理的代码。
|-----IOException
|-----FileNotFoundException
|-----ClassNotFoundException
|------运行时异常(unchecked,RuntimeException),严格来说,应该叫逻辑异常,都是自己代码的逻辑没写好,比如非空判断,类型转换判断,边界判断,都是逻辑问题。
|-----NullPointerException
|-----ArrayIndexOutOfBoundsException
|-----ClassCastException
|-----NumberFormatException
|-----InputMismatchException
|-----ArithmeticException
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8GYm8ndg-1670802982923)(C:\Users\acer\AppData\Roaming\Typora\typora-user-images\image-20210724095037371.png)]
从程序执行过程,看编译时异常和运行时异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sxgiqe81-1670802982926)(C:\Users\acer\AppData\Roaming\Typora\typora-user-images\image-20210724095254191.png)]
编译时异常:执行javac.exe命名时,可能出现的异常
运行时异常:执行java.exe命名时,出现的异常
常见的 运行时 异常
//ArithmeticException算术异常
@Test
public void test6(){
int a = 10;
int b = 0;
System.out.println(a / b);
}
//InputMismatchException输入不匹配异常
@Test
public void test5(){
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score);
scanner.close();
}
//NumberFormatException数据类型转换异常
@Test
public void test4(){
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
}
//ClassCastException类型转换异常
@Test
public void test3(){
Object obj = new Date();
String str = (String)obj;
}
//IndexOutOfBoundsException角标越界异常
@Test
public void test2(){
//ArrayIndexOutOfBoundsException数组角标越界异常
//int[] arr = new int[10];//0~9
//System.out.println(arr[10]);
//StringIndexOutOfBoundsException字符角标越界异常
String str = "abc";//012
System.out.println(str.charAt(3));
}
//NullPointerException空指针异常
@Test
public void test1(){
//int[] arr = null;
//System.out.println(arr[3]);
String str = "abc";
str = null;
System.out.println(str.charAt(0));
}
常见的 编译型 异常
@Test
public void test7(){
// File file = new File("hello.txt");
// FileInputStream fis = new FileInputStream(file);
//
// int data = fis.read();
// while(data != -1){
// System.out.print((char)data);
// data = fis.read();
// }
//
// fis.close();
}
默认的异常处理机制
当出现异常时,会启动异常处理机制,然后创建对应类型的异常对象,去匹配catch中对应的异常类型,如果没能匹配到,就启动默认的处理机制,打印异常栈信息到屏幕,程序结束;
2. 异常处理的 抓抛 模型
过程一:抛
程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类的对象。并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
异常对象的产生
- 系统自动生成的异常对象
- 手动的生成一个异常对象,并抛出(throw)
手动抛出异常(throw)
说明
在程序执行中,除了自动抛出异常对象的情况之外,我们还可以手动的throw一个异常类的对象。
2.[面试题]
throw 和 throws区别:
throw 表示抛出一个异常类的对象,生成异常对象的过程。声明在方法体内。
throws 属于异常处理的一种方式,声明在方法的声明处。
例子:
public class Test{
psvm{
try{
Student s = new Student();
s.regist(-1001);//1. 一开始来到这调用方法
sout(s);
}
catch(Exception e){//3. 接收异常对象进行
sout(e.getMessage());//输出的是手动抛出异常对象时写的数据,即“不能输入负数”
}
}
}
class Student{
private int id;
public void regist(int id) throws Exception {
if(id > 0){
this.id = id;
}else{
//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");
// throw new Exception("您输入的数据非法!");
throw new MyException("不能输入负数");//2. 到了这里,手动抛出异常对象
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
过程二:抓
可以理解为异常的处理方式:
方式一:try-catch-finally
使用说明
- 格式:
try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}catch(异常类型3 变量名3){
//处理异常的方式3
}
....
finally{
//一定会执行的代码
}
-
finally 是可选的,并不是一定要添加进去,不写finally也可以
-
使用try将可能出现异常的代码包装起来,在执行过程中,
一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配,
一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理,
一旦处理完成,就跳出当前的try-catch结构
(不管写没写finally,只要跳出try-catch之后不在报错,就可以正常运行)
-
catch中的异常类型如果没子父类关系,则谁声明在上,谁声明在下无所谓。
catch中的异常类型如果满足子父类关系,则要求子类一定声明在父类的上面。否则,报错
catch内部一定不能为空,否则捕捉了异常,但却不知道报错的地方。
-
常用的异常对象处理的方式:
- String getMessage()
- printStackTrace()(常用)
-
在try结构中声明的变量,再出了try结构以后,就不能再被调用
-
try-catch-finally结构可以嵌套
-
finally中声明的是一定会被执行的代码。
像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
总结
如何看待代码中的 编译时异常 和 **运行时异常 **?
体会1:使用try-catch-finally处理编译时异常,可以让程序在编译时就不再报错,
但是运行时仍可能报错。
相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。
体会2:开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。
针对于编译时异常,我们说一定要考虑异常的处理。
方式二:throws
"throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。
一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行!
对比两种处理方式
try-catch-finally:真正的将异常给处理掉了。
throws的方式只是将异常抛给了方法的调用者。并没真正将异常处理掉。
如何选择两种处理方式
-
如果父类中被重写的方法没throws方式处理异常,则子类重写的方法也不能使用throws,
意味着如果子类重写的方法中异常,必须使用try-catch-finally方式处理。
-
执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。
我们建议这几个方法使用throws的方式进行处理。而执行的方法a可以考虑使用try-catch-finally方式进行处理。
补充
方法重写的规则之一:
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型,否则运用多态时会因为子类选择的异常类型大于父类选择的异常类型而报错。
如何自定义一个异常类
自定义异常 常常用来满足自己的业务规格,因为jdk提供的异常不能完全满足自己的业务要求,所以就出现了自定义异常。
思考:为什么自定义异常继承的是运行时异常,而不是其他异常?
因为首先自定义异常常用来满足我们的业务要求,我们希望业务出错时能走到我们的自定义异常,而我们的业务只有去运行,去调用其他接口处理数据,去走逻辑,才能有出错的可能,才能走到我们的自定义异常,所以要继承运行时异常。
- 继承于现的异常结构:RuntimeException 、Exception
- 提供全局常量:serialVersionUID
- 提供重载的构造器
public class MyException extends Exception{
static final long serialVersionUID = -7034897193246939L;
public MyException(){
}
public MyException(String msg){
super(msg);
}
}
练习
public class ReturnExceptionDemo {
static void methodA() {
try {
System.out.println("进入方法A");//1
throw new RuntimeException("制造异常");//3
}finally {
System.out.println("用A方法的finally");//2
}
}
static void methodB() {
try {
System.out.println("进入方法B");//1
return;//3
} finally {//2
System.out.println("调用B方法的finally");
}
}
public static void main(String[] args) {
try {
methodA();//跳到方法A
} catch (Exception e) {
System.out.println(e.getMessage());
}
methodB();
}
}
综合练习
/*
编写应用程序EcmDef.java,接收命令行的两个参数,要求不能输入负数,计算两数相除。
对 数 据 类 型 不 一 致 (NumberFormatException) 、
缺 少 命 令 行 参 数(ArrayIndexOutOfBoundsException、
除0(ArithmeticException)
输入负数(EcDef 自定义的异常)进行异常处理。
*/
public class EcmDef {
public static void main(String[] args) {
try {
int i1 = Integer.parseInt(args[0]);
int j1 = Integer.parseInt(args[1]);
int result = ecm(i1,j1);
System.out.println(result);
}catch (NumberFormatException e) {
System.out.println("数据类型不一致");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("参数不齐全");
}catch (ArithmeticException e){
System.out.println("不可以除以0喔");
}catch (EcDef e){
System.out.println(e.getMessage());
}
}
public static int ecm(int i,int j) throws EcDef{
if(i<0 || j<0){
throw new EcDef("分子或分母小于0");
}
return i / j;
}
}
public class EcDef extends Exception{
static final long serialVersionUID = -33875164229948L;
public EcDef(){
}
public EcDef(String message){
super(message);
}
}
3. 异常处理的实践
3.1 不在try块中关闭资源
private static final Logger log = LoggerFactory.getLogger(Example1.class);
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./王爵的私有宝贝.txt");
inputStream = new FileInputStream(file);
// 使用 inputStream 读取文件内容
// 兄弟,不要这么做
inputStream.close();
} catch (FileNotFoundException e) {
log.error("", e);
} catch (IOException e) {
log.error("", e);
}
}
// 做法一:在finally中处理
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./王爵的私有宝贝.txt");
inputStream = new FileInputStream(file);
// 使用 inputStream 读取文件内容
} catch (FileNotFoundException e) {
log.error("", e);
} catch (IOException e) {
log.error("", e);
} finally {
if(null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
log.error("关闭流失败", e);
}
}
}
}
// 做法二:使用Try-With-Resource,jdk7之后的语法糖,会自动关闭
public void autoCloseResource() {
File file = new File("./王爵的私有宝贝.txt");
try (FileInputStream inputStream = new FileInputStream(file)){
// 使用 inputStream 读取文件内容
} catch (FileNotFoundException e) {
log.error("", e);
} catch (IOException e) {
log.error("", e);
}
}
3.2 指定具体异常
在声明方法时,指定抛出的异常类型请细致地指明,不要直接抛出Exception,方便调用方在调用出错时快速定位问题。
// 这种方式就是不推荐的
public void doNotDoThis() throws Exception {
Integer year = Integer.parseInt("biezhi???");
System.out.println("哦 " + year);
}
public void doThis() throws NumberFormatException {
Integer year = Integer.parseInt("biezhi???");
System.out.println("哦 " + year);
}
3.3 给异常加点说明
在给方法添加 JavaDoc说明文档时,请给抛出的异常简单地说明什么情况下会抛出异常。
class NotFoundGirlFriendException extends Exception {
public NotFoundGirlFriendException(String message) {
super(message);
}
}
/**
* 当心抛出 NotFoundGirlFriendException...
*
* @param input
* @throws NotFoundGirlFriendException 如果你不订阅 ... 那么😁😁😁
*/
public void doSomething(String input) throws NotFoundGirlFriendException {
}
3.4 使用描述性消息抛出异常
抛出异常时,尽可能地描述清楚异常的问题和原因,这样方便线上查看日志时快速定位到问题。
public void foo() {
try {
new Long("biezhi大法好");
} catch (NumberFormatException e) {
log.error("用户ID格式化失败", e);
}
}
3.5 优先捕获具体的异常
捕获异常的范围应该是从小到大的。
public class Example5 {
private static final Logger log = LoggerFactory.getLogger(Example5.class);
public void catchMostSpecificExceptionFirst() {
try {
doSomething("不要跟我讲这些,因为我只是一只小猫咪!");
} catch (NumberFormatException e) {
log.error("字符串转数字失败", e);
} catch (IllegalArgumentException e) {
log.error("非法参数", e);
}
}
private void doSomething(String message) {
Integer emmm = Integer.parseInt(message);
}
public static void main(String[] args) {
new Example5().catchMostSpecificExceptionFirst();
}
}
3.6 不要捕获 Throwable
thrownable是所有异常和错误的顶层父类,因为还包含着error,而error是我们没有办法处理的jvm方面的错误,所以即使捕获了,也无计可施,除非自己真的能处理这个错误。
public class Example6 {
public void doNotCatchThrowable() {
try {
// 做点什么吧
} catch (Throwable t) {
// 停下来,不要 🙅 这么干
}
}
}
3.7 不要忽略任何一个异常
只要代码中写了catch的某个类型的异常,就需要对其进行处理,不要自信地以为不会捕获到这个类型的异常,否则一旦捕获了而又没有任何处理代码,那么对于问题的定位和解决会带来很大的麻烦。
public class Example7 {
private static final Logger log = LoggerFactory.getLogger(Example7.class);
public void doNotIgnoreExceptions() {
try {
// 做点什么吧,先生
} catch (NumberFormatException e) {
// 嘤该不会走到这里(不,一定不会的)
log.error("不可能走到的", e);
}
}
}
3.8 不要同时记录并抛出异常
同时记录和抛出会在出错时同时打出多个相同的错误,会导致日志太多了。所以要么抛出,要么记录,不要两者同时进行。
public class Example8 {
private static final Logger log = LoggerFactory.getLogger(Example8.class);
public void foo() {
try {
new Long("xyz");
} catch (NumberFormatException e) {
// log.error("", e);
throw e;
}
}
public static void main(String[] args) {
new Example8().foo();
}
}
3.9 抛出自定义异常时,不要忘记原有异常
抛出自定义异常时,不要只写一些简单的错误描述,不然即使抛出异常,也无法定位代码出错的位置。需要将具体捕获的异常对象也传入自定义异常中,这样就能在控制台打印错误信息。
public class Example9 {
class MyBusinessException extends Exception {
public MyBusinessException(String message) {
super(message);
}
public MyBusinessException(String message, Throwable cause) {
super(message, cause);
}
}
public void wrapException(String id) throws MyBusinessException {
try {
long userId = Long.parseLong(id);
System.out.println("userId: " + userId);
} catch (NumberFormatException e) {
throw new MyBusinessException("描述错误的消息", e);
}
}
public static void main(String[] args) throws MyBusinessException {
new Example9().wrapException("emmm");
}
}
3.10 抛出和处理怎么选择
-
在通用的方法里,不要try去捕获错误,而是直接抛出异常给调用层处理
-
用户访问界面处理掉所有可能的异常,并记录详细错误日志,然后返回友好的错误界面给用户,不要抛异常给用户,不友好
-
异常应当在下层方法中不符合逻辑、出现异常的时候抛出,在上层进行捕获.
同样的,假使你为别人提供类库方法,在你的方法中,存在问题就应该抛出。
因为别人代码可能依赖于或者调用你的代码,在调用方可进行异常的捕获,从而能得到最原始的异常信息。
4.日志的打印
4.1 日志打印的建议
4.1.1 选择恰当的日志级别
常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要反手就是打印info哈~
- error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的;
- warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注;
- info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等;
- debug:用于开发DEBUG的,关键逻辑里面的运行时数据;
- trace:最详细的信息,一般这些信息只记录到日志文件中。
4.1.2 日志要打印方法的入参、出参
我们并不需要打印很多很多日志,只需要打印可以快速定位问题的有效日志。有效的日志,是甩锅的利器!
哪些算得的上有效关键的日志呢?比如说,方法进来的时候,打印入参。再然后呢,在方法返回的时候,就是打印出参,返回值。入参的话,一般就是userId或者bizSeq这些关键信息。正例如下:
public String testLogMethod(Document doc, Mode mode){
log.debug(“method enter param:{}”,userId);
String id = "666";
log.debug(“method exit param:{}”,id);
return id;
}
4.1.3 选择合适的日志格式
理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别,线程名字等等。在logback日志里可以这么配置:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>
</encoder>
</appender>
4.1.4 遇到if…else…等条件时,每个分支首行都尽量打印日志
当你碰到if…else…或者switch这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。
if(user.isVip()){
log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId());
//会员逻辑
}else{
log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId())
//非会员逻辑
}
4.1.5 日志级别比较低时,进行日志开关判断
对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。
User user = new User(666L, "网站", "我们");
if (log.isDebugEnabled()) {
log.debug("userId is: {}", user.getId());
}
如果配置的日志级别是warn的话,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象, 还会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议加日志开关判断。
4.1.6 不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API
SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(TianLuoBoy.class);
4.1.7 建议使用参数占位{},而不是用+拼接
logger.info("Processing trade with id: " + id + " and symbol: " + symbol);
上面的例子中,使用+操作符进行字符串的拼接,有一定的性能损耗。
logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);
我们使用了大括号{}来作为日志中的占位符,比于使用+操作符,更加优雅简洁。并且,相对于反例,使用占位符仅是替换动作,可以有效提升性能。
4.1.8 建议使用异步的方式来输出日志。
- 日志最终会输出到文件或者其它输出流中的,IO性能会有要求的。如果异步,就可以显著提升IO性能。
- 除非有特殊要求,要不然建议使用异步的方式来输出日志。以logback为例吧,要配置异步很简单,使用AsyncAppender就行
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="ASYNC"/>
</appender>
4.1.9 不要使用e.printStackTrace()
- e.printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。
- e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~
4.1.10 异常日志不要只打一半,要输出全部错误信息
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦');
}
- 异常e都没有打印出来,所以压根不知道出了什么类型的异常。
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e.getMessage());
}
- e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。
正确的使用方法
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦', e);
}
4.1.11 禁止在线上环境开启 debug
禁止在线上环境开启debug,这一点非常重要。
因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。
4.1.12 不要记录了异常,又抛出异常
log.error("IO exception", e);
throw new MyException(e);
这样实现的话,通常会把栈信息打印两次。这是因为捕获了MyException异常的地方,还会再打印一次。
这样的日志记录,或者包装后再抛出去,不要同时使用!否则你的日志看起来会让人很迷惑。
4.1.13 避免重复打印日志
避免重复打印日志,酱紫会浪费磁盘空间。如果你已经有一行日志清楚表达了意思,避免再冗余打印,反例如下:
if(user.isVip()){
log.info("该用户是会员,Id:{}",user,getUserId());
//冗余,可以跟前面的日志合并一起
log.info("开始处理会员逻辑,id:{}",user,getUserId());
//会员逻辑
}else{
//非会员逻辑
}
如果你是使用log4j日志框架,务必在log4j.xml中设置 additivity=false,因为可以避免重复打印日志
<logger name="com.taobao.dubbo.config" additivity="false">
4.1.14 日志文件分离
我们可以把不同类型的日志分离出去,比如access.log,或者error级别error.log,都可以单独打印到一个文件里面。
当然,也可以根据不同的业务模块,打印到不同的日志文件里,这样我们排查问题和做数据统计的时候,都会比较方便啦。
4.1.15 核心功能模块,建议打印较完整的日志
我们日常开发中,如果核心或者逻辑复杂的代码,建议添加详细的注释,以及较详细的日志。
日志要多详细呢?脑洞一下,如果你的核心程序哪一步出错了,通过日志可以定位到,那就可以啦。
4.1.16 只在处理异常的地方打异常日志
在抛出异常的地方打日志,然后将其重新抛出给调用者,这可能是经常做的,但这是不正确的
4.2 何时打日志
4.2.1 方法的入参出参要打
可以使用切面aop完成方法的入参出参的打印
4.2.2 远程调用其他服务方法要打
也打入参出参
4.2.3 ifelse分支要打
4.2.4 try catch中打印
捕获之后打印error级别,希望中断就直接return