前言
去年又重新刷了路遥的《平凡的世界》,最近也在朋友推荐下,来看了路遥的另一部成名作《人生》。
故事中的主人公高加林,虽生在农村,面朝黄土背朝天,却不甘心像父辈一样或者,一心想着摆脱民语的束缚,追求他的理想生活。
然而命运多舛,在他所想象的理想生活中,一次次跌倒,最终不得不承认自己的平凡,生活总得继续。
现实世界如此,代码世界里,我们也希望一切都是理想的。
我们希望用户输入的数据格式永远是正确的,打开的资源也一定存在,用户的硬件是正常的,用户的操作系统四稳定的,用户的网络也一定是畅通的等等
然而事与愿违,愿望是好的,现实是残酷的。
引入异常机制的目的就在于,当“人生”中出现异常时,能够将其捕获并处理,保证我们能更好的走下去,不至于出现一点插曲,就停滞不前。
一、异常引入
因此呢就出现了异常处理,我们把可能出现异常的业务代码放到try块中定义,把异常处理放到catch块中进行处理,保证程序遇到异常依然能继续运行,保证程序的健壮性。
① 我们来看看高加林的一生中各种异常的处理
try{ //业务逻辑,高加林的一生 System.out.println("1、高中毕业,虽然没考上大学。却不断学习,在报纸上发表诗歌和散文,参加考试,谋得一份临时教师职位"); System.out.println("2、高加林的叔叔从外地回到家乡工作,在县城担任劳动局局长,副局长为了讨好新上任的局长;"); System.out.println("便私下给高加林走了后门,就这样高加林成为了公职人员"); System.out.println("3、与高中同学黄亚萍相遇,再次相遇的两个人,志趣相投,相聊甚欢。高加林以为攀上黄亚萍这个高枝,能到大城市一展宏图"); }catch(ExceptionClass1 e1){ System.out.println("第一个异常发生,教师职位被他人顶替"); System.out.println("没有了工作,重新回到农村,成为最不想当的农民"); System.out.println("很快加入村里的劳动队伍里,白天努力劳作,晚上看书学习,等待着东山再起的机会"); }catch(ExceptionClass2 e2){ System.out.println("第二个异常发生,遭人举报走后门"); System.out.println("再次丢了工作,一直想摆脱农名身份的他,再次成为了农民"); }catch(ExceptionClass3 e3){ System.out.println("第三个异常发生,纸包不住火,黄亚萍及其家人知道了高加林遭遇举报"); System.out.println("被打回原型,和黄亚萍断绝了关系"); System.out.println("黄亚萍不想高加林回农村,希望高加林去找叔父帮忙,看是否可以继续留下来"); }catch(Exception e){ System.out.println("再次跌倒的他,没再去找叔父"); System.out.println("有了清醒的认知,自己的路还是得靠自己走下去"); }finally{ System.out.println("接受现实,更好的走下去"); }
② 代码中
//未处理异常,程序遇到异常没法继续执行 public class ExceptionTest { public static void main(String[] args) { int num1 =7; int num2 =0; int res = num1/num2; System.out.println("程序继续执行......"); } } //输出 Exception in thread "main" java.lang.ArithmeticException: / by zero at ExceptionTest.main(ExceptionTest.java:5)
加入异常处理
//加入异常处理,程序遇到异常后继续运行 public class ExceptionTest { public static void main(String[] args) { int num1 =7; int num2 =0; try { int res = num1/num2; } catch (Exception e) { System.out.println(e.getMessage()); } System.out.println("程序继续执行......"); } } //输出 / by zero 程序继续执行......
小结: 如果执行try块里的业务逻辑代码出现异常,系统会自动生成一个异常对象,该异常对象被体骄傲给Java运行时环境,这个过程成为抛出异常
Java运行时环境收到异常对象时,会寻找能处理异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被成为异常捕获;
如果Java运行环境找不到捕获异常的catch块,则运行时环境终止,Java程序已将退出
二、基本概念
程序执行中发生的不正常情况(语法错误逻辑错误不是异常)称为异常,所有的异常都继承与
java.lang.Throwable
。Throwable
有两个重要子类
Error(错误):Java虚拟机无法解决的严重问题,我们没办法通过 catch 来进行捕获 。如:内存溢出(OutOfMemoryError)、Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。error 是严重错误,Java虚拟机会选择线程终止
- Exception(异常):编程错误或偶尔的外在因素导致的一般问题,程序本身可以处理的异常,可以通过
catch
来进行捕获。Exception
又可以分 运行时异常(程序运行时发生的异常)和编译时异常(程序编译期间产生的异常,必须要处理的异常,否则代码没法编译通过)
三、异常继承体系
注:虚线为实现,实线为继承
四、常见运行异常
4.1 NullPointerException 空指针异常
public class ExceptionTest { public static void main(String[] args) { String str = null; System.out.println(str.length()); } } //输出 Exception in thread "main" java.lang.NullPointerException at ExceptionTest.main(ExceptionTest.java:4)
4.2 ArithmeticException 数学运算异常
public class ExceptionTest { public static void main(String[] args) { int num1=7; int num2=0; int res = num1/num2; } } //输出 Exception in thread "main" java.lang.ArithmeticException: / by zero at ExceptionTest.main(ExceptionTest.java:5)
4.3 ArrayIndexOutOfBoundsException 数组下标越界异常
public class ExceptionTest { public static void main(String[] args) { String strarr[] ={"个人博客","www.xiezhrspace.cn","公众号","XiezhrSpace"}; //注意数组下标时从0开始的 for (int i = 0; i <= strarr.length; i++) { System.out.println(strarr[i]); } } } //输出 个人博客 www.xiezhrspace.cn 公众号 XiezhrSpace Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4 at ExceptionTest.main(ExceptionTest.java:5)
4.4 ClassCastException 类型转换异常
public class ExceptionTest { public static void main(String[] args) { Class1 class1 = new Class2(); //向上转型 Class2 class2 = (Class2)class1; //向下转型 Class3 class3= (Class3)class1; //两个类没有关系,转型失败 } } class Class1{} class Class2 extends Class1{}; class Class3 extends Class1{}; //输出 Exception in thread "main" java.lang.ClassCastException: Class2 cannot be cast to Class3 at ExceptionTest.main(ExceptionTest.java:5)
4.5 NumberFormatException 数字格式不正确异常
public class ExceptionTest { public static void main(String[] args) { String str1 ="123"; String str2 = "abc"; System.out.println(Integer.valueOf(str1)); //可以转成功 System.out.println(Integer.valueOf(str2)); } } //输出 123 Exception in thread "main" java.lang.NumberFormatException: For input string: "abc" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.valueOf(Integer.java:766) at ExceptionTest.main(ExceptionTest.java:6)
五、常见编译时异常
编译期间就必须处理的异常,否则代码不能通过编译
.1 FileNotFoundException 操作一个不存在的文件时候发生异常
5.2 ClassNotFoundException 加载类,类不存在时异常
5.3 SQLException 操作数据库,查询表时发生异常
5.4 IllegalArgumentException 参数不匹配时发生异常
六、异常处理
Java 的异常处理通过 5 个关键字来实现:
try
、catch
、throw
、throws
和finally
。
try catch
语句用于捕获并自行处理异常
finally
语句用于在任何情况下(除特殊情况外)都必须执行的代码
throw
手动生成异常对象
throws
将发生的异常抛出,交给调用者处理,最顶级的处理者是JVM
6.1 异常处理方式
- try-catch-finally :在代码中捕获异常自行处理
- throws :将发生的异常抛出,交给调用者处理,最顶级处理者是
JVM
注: try-catch-finally 和throws 任选一种即可
6.2 异常处理6.2.1 try-catch-finally①原理图
②语法结构
try { 逻辑程序块 //可能有异常的代码 } catch(Exception e) { /* ①发生异常时,系统将异常封装成Exception对象e,并传递给catch ②得到异常Exception e 后,程序员自行处理 注:只有发生异常时候,catch代码块才执行 */ 捕获异常 throw(e); } finally { 释放资源代码块 }
③实例
小提示:选中代码,按下快捷键ctrl+alt+t 可以呼出代码提示
public class TestException { public static void main(String[] args) { int num1 =10; int num2 =0; try { int num3=num1/num2; }catch (Exception e){ e.printStackTrace(); }finally { System.out.println("finally代码块被执行了"); } System.out.println("程序继续执行..."); } } //输出 java.lang.ArithmeticException: / by zero at com.xiezhr.TestException.main(TestException.java:9) finally代码块被执行了 程序继续执行...
上述代码中,把可能出现异常的代码num1/num2; 放到了try语句块中,当代码发生异常时,在catch中捕获异常,并打印异常。不管有没有发生异常,finally 语句块里的代码都会执行。发生异常后程序并没有终止,最后输出 “程序继续执行…”
6.2.2 try-with-resources
Java中,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将>其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。
关闭资源的常用方式就是在finally块里调用close方法将资源关闭
所以对于流的操作我们经常回用到如下代码
//读取文本文件的内容 import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; public class TestException { public static void main(String[] args) { Scanner scanner = null; try { scanner = new Scanner(new File("D://xiezhr.txt")); while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException e) { e.printStackTrace(); } finally { if (scanner != null) { scanner.close(); } } } } //输出 个人博客:www.xiezhrspace.cn 个人公众号:XiezhrSpace 欢迎你关注
分析: 上述代码中我们通过io流读取D盘xiezhr.txt文件中的内容,并将其内容按行打印出来。最后在finally
代码块中将scanner关闭
Java 7 之后,新的语法糖 try-with-resources 可以简写上述代码
import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; public class TestException { public static void main(String[] args) { try(Scanner scanner = new Scanner(new File("D://xiezhr.txt"))){ while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } }catch (FileNotFoundException e){ e.printStackTrace(); } } } //输出 个人博客:www.xiezhrspace.cn 个人公众号:XiezhrSpace 欢迎你关注
两段代码实现的功能是一样的,但明显try-with-resources 写法简单了好多。我们也更提倡优先使用 try-with-resources 而不是try-finally。
6.2.3 抛出异常
当一个方法产生某种异常,但是不确定如何处理这种异常,那么就需要在该方法的头部显示地申明抛出异常,表明该方法将不对这些异常进行处理,而用该方法调用者负责处理
6.2.3.1 thorows
①语法格式
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
returnType
表示返回值类型method_name
表示方法名paramList
表示参数列表;Exception 1,Exception2,…
表示异常类
如果有多个异常类,它们之间用逗号分隔。这些异常类可以是方法中调用了可能拋出异常的方法而产生的异常,也可以是方法体中生成并拋出的异常- ②使用场景
当前方法不知道如何处理这种类型的异常,该异常应该由向上一级的调用者处理;
如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。
JVM 对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行,这就是前面程序在遇到异常后自动结束的原因
③实践操作
import java.io.File; import java.io.IOException; import java.util.Scanner; public class TestException { //定义方法时声明抛出异常,方法中出现的异常自己不处理,交由调用者处理 public void readfile() throws IOException { // 读取文件 Scanner scanner = new Scanner(new File("D://xiezhr.txt")); while (scanner.hasNext()) { System.out.println(scanner.nextLine()); } scanner.close(); } public static void main(String[] args) { TestException tt = new TestException(); try { //调用readfile方法 tt.readfile(); } catch (IOException e) { //打印异常 e.printStackTrace(); } } } //输出 个人博客:www.xiezhrspace.cn 个人公众号:XiezhrSpace 欢迎你关注
分析: 以上代码,首先在定义 readFile() 方法时用 throws
关键字声明在该方法中可能产生的异常,然后在 main()
方法中调用readFile()
方法,并使用 catch
语句捕获产生的异常
④ 异常处理流程图
注意:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
//下面程序编译就报错,原因时子类抛出比父类还大的异常
public class OverrideThrows { public void test() throws IOException { FileInputStream fis = new FileInputStream("a.txt"); } } class Sub extends OverrideThrows { // 子类方法声明抛出了比父类方法更大的异常 public void test() throws Exception { } }
6.2.3.1 throw
throw用来直接抛出一个异常
①语法
throw ExceptionObject;
- ExceptionObject 必须是 Throwable 类或其子类的对象
- 如果是自定义异常类,也必须是 Throwable 的直接或间接子类
②执行原理
throw 语句执行时,它后面的语句将不执行;
此时程序转向调用者程序,寻找与之相匹配的 catch 语句,执行相应的异常处理程序。
如果没有找到相匹配的 catch 语句,则再转向上一层的调用程序。这样逐层向上,直到最外层的异常处理程序终止程序并打印出调用栈情况
③实践操作
import java.util.Scanner; public class TestException { public boolean validateUserName(String username) { boolean con = false; if (username.length() >4) { // 判断用户名长度是否大于8位 if ("admin".equals(username)) { con = true; }else{ throw new IllegalArgumentException("你输入的用户名不对"); } } else { throw new IllegalArgumentException("用户名长度必须大于 4 位!"); } return con; } public static void main(String[] args) { TestException te = new TestException(); Scanner input = new Scanner(System.in); System.out.println("请输入用户名:"); String username = input.next(); try { boolean con = te.validateUserName(username); if (con) { System.out.println("用户名输入正确!"); } } catch (IllegalArgumentException e) { System.out.println(e); } } } //输出 ① 请输入用户名: abc java.lang.IllegalArgumentException: 用户名长度必须大于 4 位! ② 请输入用户名: abcdef java.lang.IllegalArgumentException: 你输入的用户名不对 ③ 请输入用户名: admin 用户名输入正确!
6.2.4 自定义异常
当Java提供的内置异常类型不能满足我们的需求时,我们可以设计自己的异常类型。
①语法格式
class XXXException extends Exception|RuntimeException
- 一般将自定义异常类的类名命名为 XXXException,其中 XXX 用来代表该异常的作用
- 自定义异常类需要继承 Exception 类或其子类,如果自定义运行时异常类需继承 RuntimeException 类或其子类
- 自定义异常类一般包含两个构造方法:一个是无参的默认构造方法,另一个构造方法以字符串的形式接收一个定制的异常消息,并将该消息传递给超类的构造方法。
②实践操作
import java.util.Scanner; public class TestException { public static void main(String[] args) { int age; Scanner scanner = new Scanner(System.in); System.out.println("请输入你的年龄"); age=scanner.nextInt(); try { if(age < 0) { throw new AgeException("您输入的年龄为负数!输入有误!"); } else if(age > 100) { throw new AgeException("您输入的年龄大于100!输入有误!"); } else { System.out.println("您的年龄为:"+age); } } catch (AgeException e) { e.printStackTrace(); } } } //输出 ① 请输入你的年龄 120 com.xiezhr.AgeException: 您输入的年龄大于100!输入有误! at com.xiezhr.TestException.main(TestException.java:15) ② 请输入你的年龄 -34 com.xiezhr.AgeException: 您输入的年龄为负数!输入有误! at com.xiezhr.TestException.main(TestException.java:13) ③ 请输入你的年龄 30 您的年龄为:30
6.2.5 多异常捕获
Java7以后,catch 语句可以有多个,用来匹配多个异常
①语法格式
try{ // 可能会发生异常的语句 } catch (IOException | ParseException e) { // 异常处理 }
- 多种异常类型之间用竖线|隔开
- 异常变量有隐式的 final 修饰,因此程序不能对异常变量重新赋值
② 异常书写方式变化
try{ // 可能会发生异常的语句 } catch (FileNotFoundException e) { // 调用方法methodA处理 } catch (IOException e) { // 调用方法methodA处理 } catch (ParseException e) { // 调用方法methodA处理 }
变成
try{ // 可能会发生异常的语句 } catch (FileNotFoundException | IOException | ParseException e) { // 调用方法处理 }
③实践操作
import java.util.Scanner; public class TestException { public static void main(String[] args) { int num1; int num2; try { Scanner scanner = new Scanner(System.in); System.out.println("请输入num1的值"); num1=scanner.nextInt(); System.out.println("请输入num2的值"); num2=scanner.nextInt(); int num= num1/num2; System.out.println("你输入的两个数相除,结果是" + num); } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException e){ System.out.println("程序发生了数组越界、数字格式异常、算术异常之一"); e.printStackTrace(); } catch (Exception e) { System.out.println("未知异常"); e.printStackTrace(); } } } //输出 ① 请输入num1的值 12 请输入num2的值 0 程序发生了数组越界、数字格式异常、算术异常之一 java.lang.ArithmeticException: / by zero at com.xiezhr.TestException.main(TestException.java:15) ② 请输入num1的值 6888888888 未知异常 java.util.InputMismatchException: For input string: "6888888888" at java.util.Scanner.nextInt(Scanner.java:2123) at java.util.Scanner.nextInt(Scanner.java:2076) at com.xiezhr.TestException.main(TestException.java:12) ③ 请输入num1的值 12 请输入num2的值 4 你输入的两个数相除,结果是3
分析:
上面程序中IndexOutOfBoundsException|NumberFormatException|ArithmeticException
来定义异常类型,这就表明该 catch 块
可以同时捕获这 3 种类型的异常
七、Throwable 类常用方法
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
八、易混概念
8.1 Error和Exception的异同
Error
和Exception
都有共同的祖先Throwable
,即Error
和Exception
都是Throwable
的子类Exception
:程序本身可以处理的异常,可以通过try-catch
来进行捕获。Exception
又可以分为Checked Exception
(受检查异常,必须处理) 和Unchecked Exception
(不受检查异常,可以不处理)。
Error
:Error
属于程序无法处理的错误 ,我们没办法通过 try-catch
来进行捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止
8.2 throw和throws的区别
throws
用来声明一个方法可能抛出的所有异常信息,表示出现异常的一种可能性,但并不一定会发生这些异常;throw
则是指拋出的一个具体的异常类型,执行throw
则一定抛出了某种异常对象。- 通常在一个方法(类)的声明处通过
throws
声明方法(类)可能拋出的异常信息,而在方法(类)内部通过throw
声明一个具体的异常信息。 throws
通常不用显示地捕获异常,可由系统自动将所有捕获的异常信息抛给上级方法;throw
则需要用户自己捕获相关的异常,而后再对其进行相关包装,最后将包装后的异常信息抛出
8.3 Checked Exception 和 Unchecked Exception
Checked Exception
受检查异常 Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译- 常见的受检查异常有: IO 相关的异常、
ClassNotFoundException
、SQLException
- 例如,下面就是
Unchecked Exception
即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。- 我们经常看到的有以下几种
NullPointerException
:空指针错误 IllegalArgumentException
:参数错误比如方法入参类型错误
NumberFormatException
:字符串转换为数字格式错误
ArrayIndexOutOfBoundsException
:数组越界错误
ClassCastException
:类型转换错误
ArithmeticException
:算术错误
SecurityException
:安全错误比如权限不够
UnsupportedOperationException
:不支持的操作错误比如重复创建同一用户
8.4 try-with-resources 与 try-catch-finally
try-with-resources
是Java 1.7增加的新语法糖,在try
代码块结束之前会自动关闭资源。try-with-resources
适用于任何实现java.lang.AutoCloseable
或者java.io.Closeable
的对象, 字节输入流(InputStream
),字节输出流(OutputStream
),字符输入流(Reader
),字符输出流(Writer
)均实现了这接口try-catch-finally
没有限制条件,finally
不仅可以关闭资源,还可以用于执行其他代码块;try-with-resources
代码更加简洁,有限制条件,资源会立即被关闭
finally
关闭资源不会立即关闭,取决与网络和系统,可能会很快,也可能会等一两天,所以,最好不要使用finally作为业务流程的控制,
- 在《Effective java》一书 的第9条:try-with-resources优先于try-finally 中有相关详细的介绍,其中提到了许多由于finally延迟导致的网络事件
九、SpringBoot 中优雅的处理统一异常返回
日常开发中,我们处理异常一般都会用到try-catch
、throw
和throws
的方式抛出异常。
这种方式不经程序员处理麻烦,对用户来说也不太友好
我们都希望不用写过多的重复代码处理异常,又能提升用户体验。这时候全局异常处理就显得很便捷很重要了
9.1 全局异常捕获与处理
Springboot对提供了一个
@ControllerAdvice
注解以及@ExceptionHandler
注解,分别用于开启全局的异常捕获和说明捕获哪些异常,对那些异常进行处理。
@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(value =Exception.class) public String exceptionHandler(Exception e){ System.out.println("出现了一个异常"+e); return e.getMessage(); } }
析 上面这段代码就是说,只要是代码运行过程中有异常就会进行捕获,并输出出这个异常。然后我们随便编写一个会发生异常的代码,测试出来的异常是这样的。
这对于前后端分离来说这样的报错对用户并不好,前后端分离之后唯一的交互就是json了,我们也希望将后端的异常变成json返回给前端处理。
9.2 用枚举类型记录已知错误信息与成功信息
ErrorEnum
枚举类中定义了常见的错误码以及错误的提示信息。
SuccEnum
枚举类中定义了成功码及成功提示信息
至于这里为什么用枚举就不具体说了,网上文章说说的也比较多了
具体可以参照:Java 枚举(enum) 详解7种常见的用法
① 已知错误信息
public enum ErrorEnum { // 数据操作错误定义 NO_PERMISSION(403,"没有权限访问"), NO_AUTH(401,"请先登录系统"), NOT_FOUND(404, "未找到该资源!"), USER_NOT_FIND(402, "未找到用户信息"), INTERNAL_SERVER_ERROR(500, "服务器出问题了"), UNKNOW_ERR(-1,"未知错误") ; /** 错误码 */ private Integer errorCode; /** 错误信息 */ private String errorMsg; ErrorEnum(Integer errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } public Integer getErrorCode() { return errorCode; } public String getErrorMsg() { return errorMsg; } }
② 成功信息
public enum SuccEnum { SUCCESS(200, "success"); /** 成功码 **/ private Integer succCode; /* 成功信息*/ private String succMsg; SuccEnum(Integer succCode, String succMsg) { this.succCode = succCode; this.succMsg = succMsg; } public Integer getSuccCode() { return succCode; } public String getSuccMsg() { return succMsg; } }
9.3 定义统一结果返回与异常返回
success
:用boolean
类型标识,标识是否成功code
: 状态码,区分各种报错信息与成功返回msg
: 成功或错误提示信息data
: 返回的数据
@Data public class Result<T> { //是否成功 private Boolean success; //状态码 private Integer code; //提示信息 private String msg; //数据 private T data; public Result() { } //自定义返回结果的构造方法 public Result(Boolean success,Integer code, String msg,T data) { this.success = success; this.code = code; this.msg = msg; this.data = data; } }
9.4 封装工具类返回结果
这里我们定义好了统一的结果返回,其中里面的静态方法是用来当程序异常的时候转换成异常返回规定的格式。
public class ResultUtil { //成功,并返回具体数据 public static Result success(SuccEnum succEnum,Object obj){ Result result = new Result(); result.setSuccess(true); result.setMsg(succEnum.getSuccMsg()); result.setCode(succEnum.getSuccCode()); result.setData(obj); return result; } //成功,无数据返回 public static Result succes(SuccEnum succEnum){ Result result = new Result(); result.setSuccess(true); result.setMsg(succEnum.getSuccMsg()); result.setCode(succEnum.getSuccCode()); result.setData(null); return result; } //自定义异常返回的结果 public static Result defineError(DefinitionException de){ Result result = new Result(); result.setSuccess(false); result.setCode(de.getErrorCode()); result.setMsg(de.getErrorMsg()); result.setData(null); return result; } //其他异常处理方法返回的结果 public static Result otherError(ErrorEnum errorEnum){ Result result = new Result(); result.setSuccess(false); result.setMsg(errorEnum.getErrorMsg()); result.setCode(errorEnum.getErrorCode()); result.setData(null); return result; } }
9.5 自定义异常
内置异常不能满足我们业务需求的时候,我们就需要自定义异常
public class DefinitionException extends RuntimeException { protected Integer errorCode; protected String errorMsg; public DefinitionException(){ } public DefinitionException(Integer errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } public Integer getErrorCode() { return errorCode; } public void setErrorCode(Integer errorCode) { this.errorCode = errorCode; } public String getErrorMsg() { return errorMsg; } public void setErrorMsg(String errorMsg) { this.errorMsg = errorMsg; } }
9.6 定义全局异常处理类
我们自定义一个全局异常处理类,来处理各种异常,包括自己定义的异常和内部异常。这样可以简化不少代码,不用自己对每个异常都使用try,catch的方式来实现
@ControllerAdvice public class GlobalExceptionHandler { /** * 处理自定义异常 * */ @ExceptionHandler(value = DefinitionException.class) @ResponseBody public Result bizExceptionHandler(DefinitionException e) { return ResultUtil.defineError(e); } /** * 处理其他异常 * */ @ExceptionHandler(value = Exception.class) @ResponseBody public Result exceptionHandler( Exception e) { return ResultUtil.otherError(ErrorEnum.UNKNOW_ERR); } }
说明: 方法上面加上一个 @ResponseBody
的注解,用于将对象解析成json,方便前后端的交互,也可以使用 @ResponseBody
放在异常类上面
9.7 代码测试
9.7.1 定义User实体类
@Data public class User { //唯一标识id private Integer id; //姓名 private String name; //性别 private String sex; //年龄 private Integer age; }
9.7.2 定义controller类
@RestController @RequestMapping("/result") public class ExceptionController { @Autowired private GlobalExceptionHandler globalExceptionHandler; @GetMapping("/getUser") public Result getStudent(){ User user = new User(); user.setId(100); user.setName("xiezhr"); user.setAge(21); user.setSex("男"); Result result = ResultUtil.success(SuccEnum.SUCCESS, user); return result; } @GetMapping("/getDefException") public Result DeException(){ throw new DefinitionException(400,"我出错了"); } @GetMapping("/getException") public Result Exception(@RequestParam("name") String name, @RequestParam("pwd") String pwd){ Result result = ResultUtil.success(SuccEnum.SUCCESS); try { if ("admin".equals(name)){ User user = new User(); user.setId(101); user.setName("xiezhr"); user.setAge(18); user.setSex("男"); result = ResultUtil.success(SuccEnum.SUCCESS,user); }else if (name.equals("xiezhr")){ result = ResultUtil.otherError(ErrorEnum.USER_NOT_FIND); }else{ int i = 1/0; } }catch (Exception e){ result = globalExceptionHandler.exceptionHandler(e); } return result; } }
9.8 接口测试
9.8.1 获取没有异常的数据返回
http://localhost:8090/result/getUser
9.8.2 自定义异常返回
http://localhost:8090/result/getDefException
http://localhost:8090/result/getException?name=xiezhr&pwd=123
9.8.3 其他的异常 返回
http://localhost:8090/result/getException?name=ff&pwd=abc
十、异常处理及规约
异常的处理⽅式有两种。 1、 ⾃⼰处理。 2、 向上抛, 交给调⽤者处理。
异常, 千万不能捕获了之后什么也不做。 或者只是使⽤e.printStacktrace。
具体的处理⽅式的选择其实原则⽐较简明: ⾃⼰明确的知道如何处理的, 就要处理掉。
下面时阿里巴巴Java开发手册关于异常处理规则
①【强制】 Java类库中定义的可以通过预检查方式规避的RuntimeException
不应该通过catch
的方式处理,如NullPointerException
、IndexOutOfBoundsException
等
说明:无法通过预检查的异常不在此列,比如当解析字符串形式的数字时,可能存在数字格式错误,通过catch NumberFormatException
实现
正例:
if(obj!=null){....}
反例:
try{ obj.method(); }catch(NullPointerException e){ ... }
②【强制】 异常捕获后不要用来做流程控制和条件控制
说明:异常设计的初衷是解决程序运行中各种意外,且异常的处理效率比条件判断方式要第很多。
③【强制】 catch
时请分清稳定代码和非稳定代码。稳定代码一般指本机运行且执行结果确定性高的代码。对于非稳定代码的catch
尽可能在进行异常类型的分区后,再做对应的异常处理
说明:对大段代码进行try-catch
,将使程序无法根据不同的异常做出正确的“应激”反应,也不利于定位问题,这是一种不负责的表现
正例:在用户注册场景中,如果用户输入非法字符串,或用户名称已存在,或用户输入的密码过于简单,那么程序会作出分门别类的判断并提示用户。
④【强制】 捕获异常使为了处理异常,不要捕获了却说明都不处理而抛弃之,如果不想处理它,请将异常抛给它的调用者。最外层的业务使用者必须处理异常,将其转换为用户可以理解的内容。
⑤【强制】 在事务场景中,抛出异常被catch
后,如果需要回滚,那么一定要注意手动回滚事务。
⑥【强制】finally
块必须对资源对象、流对象进行关闭操作,如果有一次要做try-catch
操作.
说明:对于JDK7即以上版本,可以使用try-catch-resource
方式
⑦【强制】 不要在finally
块中使用return
说明try
块中return
语句执行成功后,并不马上返回,而是继续执行finally
块中的语句,如果此处存在return
语句,则在此直接返回,无情地丢弃try
块中的返回点。
正例:
private int x =0; public int checkReturn(){ try{ //x=1,此处不返回 return ++x; }finally{ //返回的结果是2 return ++x; } }
⑧【强制】 捕获异常与抛出异常必须完全匹配,或者捕获异常时抛出异常的父类。
说明:如果以及对方抛出的时绣球,实际接收到的时铅球,就会产生意外
⑨【强制】 在调用RPC、二方包或动态生成类的相关方法时,捕获异常必须使用Throwable拦截。
说明:通过反射机制调用方法,如果找不到方法,则抛出
NoSuchMethodException
。在说明情况下抛出
NoSuchMethodException
呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。对于这些情况,即使在代码编译期是正确的,在代码运行期也会抛出
NoSuchMethodException
⑩【推荐】 方法的返回值可以为null
,不强制返回空集合或者空对象等,必须添加注释充分说明在说明情况下会返回null
值。此时数据库id
不支持存入负数二抛出异常。
说明:本手册明确,防止产生NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑远程调用失败、序列化失败、运行时异常等场景返回null
值的情况
⑪【推荐】 防止产生NPE时程序员的基本修养,注意NPE产生的场景。
1)当返回类型为基本数据类型,return
包装数据类型的对象时,自动拆箱有可能产生NPE
反例:
public int f(){ //如果为null,则自动拆箱,抛NPE。 return Integer 对象; }
2)数据库的查询结果可能为null
3) 集合里的元素即使isNotEmpty , 取出的数据元素也有可能为null
4) 当远程调用返回对象时,一律要求进行空指针判断,以防止产生NPE。
5)对于Session 中获取的数据,建议进行NPE检查,以避免空指针
6)级联调用obj.getA().getB().getC();的一连串调用容易产生NPE。
⑫【推荐】 定义时区分 unchecked/checked
异常,避免直接抛出new RuntimeException()
,更不允许抛出Exception
或者Throwable
,应该使用业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException
等
⑬ 【参考】 对于公司外的HTTP/API
开放接口,必须使用“errorCode”
:应用内部推荐异常抛出;
跨应用间RPC调用优先考虑使用Result
方式,封装isSuccess()
方法、errorCode
和errorMessage
。
说明:关于RPC方法返回方式使用Result方式的理由
1)使用抛出异常返回方式,调用方式如果没有捕获到,就会产生运行时错误
2)如果不加栈信息,知识new
自定义异常,加入自己理解的errorMesage
,对于调用解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
⑭【参考】 避免出现重复的代码(Don't Repeat Yourself
),即DRY原则
说明: 随意复制和粘贴代码,必然导致代码的重复,当以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法或公共类,甚至将代码组件化。
正例: 一个类中由多个public
方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto){...}
本期内容到这就结束了,各位小伙伴们,我们下期见 (●’◡’●)