Bootstrap

Java异常管理

1.广义异常(Throwable)的继承体系

两大类:不可通过程序恢复中断的可通过程序恢复中断的

Java中所有“程序问题”的父类是java.lang.Throwable类,所有的异常类型都是它的子类,它的子类按照是否可通过程序中断的特点,分为两个体系

Error:不可通过程序恢复中断的。当发生类似异常时,通常认为造成错误的原因是由于硬件原因,通过程序即使恢复了运行,程序也会由于硬件原因无法执行。所以此类异常发生,只能等待硬件的修复。

Exception:可通过程序恢复中断的。当发生此类异常时,通常认为程序的逻辑不严谨,可以通过排查程序逻辑进行处理。

2.狭义异常(Exception)的分类

两类:     运行时异常(RuntimeException)

                检查时异常(除了RuntimeException类及其子类以外的其他Exception子类)

运行时异常的特点是编译时不强制要求对异常发生给出预案,一旦代码逻辑错误,程序运行时会发生异常。

常见的运行时异常:

        数学运算异常ArithmeticException,整数除法或取余操作时除数为0。

        空指针引用异常NullPointerException,调用了null值对象的属性和方法

        类型强制转换异常ClassCastException,下溯造型逻辑错误

        下标越界异常IndexOutOfBoundsException,数组或字符串下标越界

        非法参数异常IllegalArgumentException,在框架的一些数据绑定过程中绑定了错误的参数

检查性异常的特点是编译时强制要求给出异常发生时的预案,一旦运行时发生异常,立即执行预案。需要注意的是,检查性异常不代表异常一定发生,它只是要求运行含有它的代码时,必须给出异常发生时的处理预案。

         对于运行时异常,绝大部分都是由于程序的逻辑错误造成的,所以运行时异常一般通过完善程序逻辑来 处理,比如发生空指针异常时我们应该做的不是进行异常处理,而是检查空指针发生的原因,从源头规 避null对象的产生。或者对null对象有一套专门的处理逻辑。

         检查性异常发生的原因可能不是逻辑错误,此时我们需要通过设置预案来防止程序因为异常造成的中 断,恢复程序的继续执行。异常的预案有两种方式:抛出(上报)和捕获

3.异常管理-抛出机制

异常的第一个处理机制是抛出,也叫上报,其作用是如果发生了异常,不进行任何处理,而是将异常抛出给调用异常所在方法的调用者,由方法调用者来进行处理。

某个方法如果需要抛出异常,需要通过throws关键字在方法上声明抛出了哪些异常,方法的调用者需要对异常进行处理(继续抛出或就地捕获)

例:调用线程休眠方法会抛出线程中断异常InterruptedException

public static void test() throws InterruptedException{
    Thread.sleep(1000);
}

public static void main(String[] args){
    test();//调用test方法会要求main方法强制处理InterruptedException
}

某些情况下我们可能还需要人为制造异常,提示方法调用者程序可能会出现问题,通过 throw 关键字抛 出一个异常对象。

示例:设计一个方法,输入两个正整数,返回它们的和。如果输入参数不是正整数,抛出异常提示方法 调用者

public static int add(int a,int b) throws Exception{
    if(a<0||b>0){
        //参数不正确
        throw new Exception();//人为制造异常
    }
    return a+b;
}

人为制造异常很多时候是为了配合项目的整体异常管理,项目中可能对异常有全局统一的管理,根据异常生成相应的UI显示,所以发生问题我们可能会抛出异常。

4.异常管理-捕获机制

如果任由异常持续的抛出,实际上最终没有任何程序对异常进行了处理,那么异常最终会抛给JVM。 JVM只能将异常的栈信息打印出来,这与异常没有处理的性质是一样的。

完善的程序应该在合理的时机抛出异常,合理的时机进行捕获异常,让程序的执行得到恢复。异常的捕 获由以下关键字相关代码块组成

try...catch...finally...

它们的使用有如下规则:

  • 必须有try代码块,且一组异常处理中只有一个try,
  • try不能单独使用,必须与至少一个catch或finally块组合使用
  • catch代码块的数量任意(可以没有),表述异常类型范围越大的catch应该在多个catch的越下方
  • finally最多只能写一个

一个比较标准的捕获处理如下:

try{
    //可能发生异常的代码
}catch(IOException e1){
    //发生IO异常时的处理程序,此处可以是任意其他异常类型,也可以没有这块
} catch(Exception e2){
    //发生其他异常时的处理程序,此处可以是任意其他异常类型,也可以没有这块
}finally{
    //无论是否发生异常,都要执行的代码
}

这段代码的实际执行如下

  • 当try中没有发生任何异常,将try中的代码执行完毕,然后执行finally的代码
  • 当try中发生了IOException异常,执行try发生异常前的代码,然后执行IO异常的catch,最后是 finally
  • 当try中发生了其他异常,执行try发生异常前的代码,然后执行Exception的catch,最后是finally

总结一下

  • try中编写可能发生异常的代码 
  • catch中编写异常发生时的预案代码
  • finally中编写无论异常是否发生,都要执行的代码。

示例:异常抛出和捕获

public static void test() throws InterruptedException,FileNotFoundException {
    Thread.sleep(1000);
    FileInputStream fis = new FileInputStream("c:/1.txt");
}
public static void main(String[] args) {
    try {
        test();
    } catch (FileNotFoundException e) {
        System.out.println("文件没有找到");
    } catch (InterruptedException e) {
        System.out.println("线程意外中断");
    }
    try {
        test();
    } catch (FileNotFoundException | InterruptedException e) {
        System.out.println("发生了异常");
    }
    try {
        test();
    }catch (Exception e) {
        System.out.println("发生了异常");
    }
    try {
        test();
    } catch (FileNotFoundException e) {
        System.out.println("文件没有找到");
    } catch (InterruptedException e) {
        System.out.println("线程意外中断");
    } catch (Exception e) {
        System.out.println("发生了异常");
    }
}

5.自定义异常

自定义异常:继承Throwable或者它的子类Exception的用户自己定义的异常类。

自定义异常的步骤:

  1.  创建自定义异常类
  2. 在方法中通过throw抛出异常对象
  3. 如果在当前方法中对抛出的异常对象作处理,可以使用try-catch语句块捕获抛出异常对象并且处 理,否则要在方法的声明处通过throws关键字指明要抛出给方法调用者的异常。
  4. 在出现异常方法的调用者中捕获并处理异常
class CircleException extends Exception { //自定义异常类
    double radius;
    CircleException(double r) { //有参构造方法
        radius=r;
    }
    public String toString() {
        return "半径 r="+radius+"不是一个正数";
    }
}
class Circle{
    private double radius;
    public void setRadius(double r) throws CircleException{
        if(r<0){
            throw new CircleException(r);
        }else{
            radius=r;
        }
    }
    public void show(){
        System.out.println("圆面积="+3.14*radius*radius);
    }
}
public class Demo01{
    public static void main(String args[]){
        Circle cir=new Circle();
        try{
            cir.setRadius(-2);
        }catch(CircleException e){
            System.out.println("自定义异常:"+e.toString()+" ");
        }
        cir.show();
    }
}

6.Try-With-Resources

Java库中有很多资源需要手动关闭,例如打开的文件、连接的数据库等。在java7之前都是try-finally 的方式关闭资源,try后面总是跟着一个“{”。

try-finally 块进行资源使用及手动关闭的方式存在几个问题:

  • 容易忘记关闭资源,从而引发内存泄漏;
  • 资源比较多的时候,代码嵌套层次较深,代码可读性不佳;
  • try 块与 finally 块同时发生异常时,存在异常压制问题。
import java.io.*;
public class Demo01 {
    public static void main(String[] args) {
        copy("D:/A.java","E:/B.Java");//复制文件
    }
    private static void copy(String src, String des) {
        InputStream in = null;
        OutputStream out = null;
        try{
            in = new FileInputStream(src);
            out = new FileOutputStream(des);
            byte[] buff = new byte[1024];//创建一个长度为1024字节的字节数组
            int n;
            //从输入流一次最多流入buff.length个字节的数据到buff中,直到文件末尾结束
            while ((n=in.read(buff))>=0){
                //将数组buff中的数据从0位置开始,长度为n的字节输出到输出流中
                out.write(buff,0,n);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(in!=null){
                try{
                in.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            if(out!=null){
                try{
                    out.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }
    }    
}

上面实现过程非常的杂乱冗长。

Java7之后,推出了Try-With-Resources声明代替之前的方式,try后跟括号"(",括号内的部分称为资源 规范头。资源规范头中可以包含多个定义,通过分号进行分隔。规范头中定义的对象必须实现 java.lang.AntoCloseable 接口,这个接口中有一个 close() 方法,因此无论是否正常退出try语句 块,这些对象都会在try语句块运行结束之后调用close方法,从而替代以前的在finally中关闭资源的功 能,且不需要冗长的代码,另外,Try-With-Resources中的try语句可以不包含catch或者finally语句块 而独立存在。

public class Demo01{
    public static void main(String[] args){
        copy("D:/A.java","E:/B.Java");//复制文件
    }
    
    private static void copy(String src,String des){
        try(InputStream in = new FileInputStream(src);
            OutputStream out = new FileOutputStream(des)) {
            byte[] buff =new byte[1024];//创建一个长度为1024字节数组
            int n;
            //从输入流一次最多流入buff.length个字节的数据到buff中,直到文件末尾结束
            while((n=in.read(buff))>=0){
                //将数组buff中的数据从0位置开始,长度为n的字节输出到输出流中
                out.write(buff,0,n);
            }
        }catch(IOException e){
                e.printStackTrace();
        }
    }
}

7.三层架构异常处理

1. dao层不捕获异常、不抛出异常:spring框架将底层的数据库checked异常封装成unchecked异常 了

2. service层捕获异常,并抛出自定义unchecked异常,抛出的异常定义状态码:checked异常默认 情况事务不会回滚 . (如果不抛出异常事务不会回滚)

3. controller层捕获unchecked异常, 处理返回结果.

    @PostMapping("/backup")
    @ApiOperation(value = "数据备份")
    @ApiResponses(
        @ApiResponse(code = 200,message = "备份成功")
    )
    public ResponseResult dataBackUp(@RequestParam String onlineUserName){
        try {
            if (iMrvDataBackupService.dataBackup(onlineUserName)){
                return ResponseResult.success();
            }
            return ResponseResult.fail("备份异常");
        }catch (Exception e){
            log.error("备份异常", e);
            return ResponseResult.fail("备份异常,"+e.getMessage());
        }
}

注意:SpringBoot项目全局异常处理:一般在使用@ControllerAdvice+@ExceptionHandler 全局统一处理并捕获异常返回前端。

;