1.概述

定义:程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常

异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰。

异常体系结构

image-20241123113202624

Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。Throwable又派生出Error类和Exception类。

error:Error类以及它的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理

如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢,内存溢出等

异常:Exception以及它的子类,代表程序运行时发生的各种不期望发生的事件。可以被java异常处理机制处理,是异常处理的核心。

(例如: 空指针访问,试图读取不存在的文件, 网络连接中断,数组越界)

exception又分为运行时异常和受检异常

受检异常

当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检异常

运行时异常

都是RuntimeException类及其子类异常,如IOException、SQLException等以及用户自定义的Exception异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起程序应该从逻辑角度尽可能避免这类异常的发生。

Why:为什么有非检查异常?

    你想想非检查异常都有哪些?NullPointerException,IndexOutOfBoundsException,VirtualMachineError等,这些异常你编译的时候检查吗?再说了,明明可以运行时检查,都在编译的时候检查,你写的代码还能看吗?而且有些异常只能在运行时才能检查出来,比如空指针,堆溢出等。

2.处理异常的机制

异常追踪栈

异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。

异常最先发生的地方,叫做异常抛出点)

image-20241123113220517

当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用它的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。

异常处理的处理机制

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0,数据为空,输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长、臃肿,可读性差。因此采用异常处理机制。

对于检查异常/非检查异常,都有2种不同的处理方式: 1、try...catch...finally语句块。 2、在函数签名中使用throws 声明交给函数调用者caller去解决。

抛出异常: Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常。

异常对象的生成: 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,如果在当前代码中没有找到相应的处理程序,就会在后台自动创建一个对应异常类的实例对象并抛出——自动抛出。 由开发人员手动创建:Exception exception = new ClassCastException();——创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样。

捕获(catch)异常: 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常。

异常处理机制一:throws

使用方法略

需要注意的地方

1、try块中的局部变量和catch块中的局部变量(包括异常变量),以及finally中的局部变量,他们之间不可共享使用

2、每一个catch块用于处理一个异常。异常匹配是按照catch块的顺序从上往下寻找的,只有第一个匹配的catch会得到执行、如果同一个try块下的多个catch异常类型有父子关系,应该将子类异常放在前面,父类异常放在后面,这样保证每个catch块都有存在的意义。 3、在try块中打开资源,在finally块中清理释放这些资源。

异常处理机制二:throws

throws 在方法签名中,声明方法可能抛出什么异常,让调用者来处理这些异常

throws声明:如果一个方法内部的代码会抛出检查异常(checked exception),而方法自己又没有完全处理掉或并不能确定如何处理这种异常,则javac保证你必须在方法的签名上使用throws关键字声明这些可能抛出的异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理,否则编译不通过。

手动抛出异常:throw

Java异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出。

在java中,提供了一个 throw 关键字,它用来抛出一个指定的异常对象。 在当前方法中不处理,抛给调用者处理。

throw语句的后面可以抛出的异常必须是Throwable或其子类的实例。

throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点

3.异常的链化

假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常,但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。

异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫它根源异常(cause)。

4.自定义异常

如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展(extends)自RuntimeException。

按照国际惯例,自定义的异常应该总是包含如下的构造函数: 一个无参构造函数 一个带有String参数的构造函数,并传递给父类的构造函数。 一个带有String参数和Throwable参数,并都传递给父类构造函数。 一个带有Throwable 参数的构造函数,并传递给父类的构造函数。

public IOException() {
    super();
}
 
public IOException(String message) {
    super(message);
}
 
public IOException(String message, Throwable cause) {
    super(message, cause);
}
 
public IOException(Throwable cause) {
    super(cause);
}

自定义异常构建

首先写一个自定义异常,继承Exception,代码如下:

public class MyException extends Exception {

public MyException() {
    super();
}
 
public MyException(String message) {
    super(message);
}
 
public MyException(String message, Throwable cause) {
    super(message, cause);
}

public MyException(Throwable cause) {
    super(cause);
}

使用自定义异常

public class Test { public static void main(String[] args) { A a = new A(); try { a.show(-2); } catch (MyException e) { System.out.println(e.getMessage()); } } }

class A { public void show(int num) throws MyException { if (num < 0) { MyException me = new MyException("异常:" + num + "不是正数"); throw me;//抛出异常,结束方法show()的执行 } System.out.println(num); }

}

· ··对于需要处理的可能异常,判断后抛出

class AgeException extends Exception { public AgeException(String message) { super(message); } }

public class Example6_9 { public static void input() { Scanner scanner = new Scanner(System.in); System.out.println("请输入姓名:"); String name = scanner.next();//接收键盘上姓名的输入 System.out.println("请输入年龄:");

    while (scanner.hasNext()) {
        try {
            int age = scanner.nextInt();//接收键盘上年龄的输入
            if (age < 0) {
                throw new AgeException("年龄不能为负数");
            }
            System.out.println("姓名" + name);
            System.out.println("年龄" + age);
            break;
 
        } catch (AgeException e) {
            System.out.println(e.getMessage() + "请重新输入:");
        }
    }
}

5.全局异常处理

//方法增强,只有@RsetController注解下的方法才会调用这个异常处理模式
@RestControllerAdvice(annotations = RestController.class)
//这个注解开启日志功能
@Slf4j
//这个注解注入到spring容器
@Component
public class GlobalExpectionHandler {
​
    @ExceptionHandler(MethodArgumentNotValidException.class)
    //处理参数效验的异常
    public Result handMethodArgumentNotValidException(MethodArgumentNotValidException e){
        
        //把异常对象e的message变成字符串,在日志上输出
        log.error("参数效验异常!",e);
        
        //给前端返回结果
        return new Result(401,e.getMessage(),null);
    }
    
}

RestControllerAdvice aop处理,对于controller层抛出的异常(抛给MethodArgumentNotValidException,这个自定义异常类)进行统一处理