作者:王迪(惜鸟)

概述

最近在代码CR的时分发现一些值得注意的问题,特别是在对Java反常处理的时分,比方有的同学对每个办法都进行 try-catch,在进行 IO 操作时忘记在 finally 块中封闭连接资源等等问题。回想自己对 java 的反常处理也不是特别清楚,看了一些反常处理的标准,并没有进行体系的学习,所以为了对 Java 反常处理机制有更深化的了解,我查阅了一些材料将自己的学习内容记载下来,希望对有相同困惑的同学供给一些帮助。

在Java中处理反常并不是一个简略的事情,不仅仅初学者很难了解,即便一些有经历的开发者也需求花费许多时间来考虑怎么处理反常,包括需求处理哪些反常,怎样处理等等。

在写本文之前,经过查阅相关材料了解怎么处理Java反常,首要查看了阿里巴巴Java开发标准,其间有15条关于反常处理的阐明,这些阐明告知了咱们应该怎么做,可是并没有具体阐明为什么这样做,比方为什么引荐运用 try-with-resources 封闭资源 ,为什么 finally 块中不能有 return 句子,这些问题当咱们从字节码层面剖析时,就能够十分深刻的了解它的实质。

经过本文的的学习,你将有如下收获:

  • 了解Java反常的分类,什么是查看反常,什么对错查看反常
  • 从字节码层面了解Java的反常处理机制,为什么finally块中的代码总是会履行
  • 了解Java反常处理的不标准事例
  • 了解Java反常处理的最佳实践
  • 了解项目中的反常处理,什么时分抛出反常,什么时分捕获反常

Java 反常处理机制

1、java 反常分类

Java异常处理和最佳实践(含案例分析)

总结:

  • Thorwable类(表明可抛出)是一切反常和过错的超类,两个直接子类为Error和Exception,分别表明过错和反常。

  • 其间反常类Exception又分为运转时反常(RuntimeException)和非运转时反常, 这两种反常有很大的区别,也称之为非查看反常(Unchecked Exception)和查看反常(Checked Exception),其间Error类及其子类也对错查看反常。

查看反常和非查看反常

  • 查看反常:也称为“编译时反常” ,编译器在编译期间查看的那些反常。由于编译器“查看”这些反常以保证它们得到处理,因此称为“查看反常”。假如抛出查看反常,那么编译器会报错,需求开发人员手动处理该反常,要么捕获,要么从头抛出。除了RuntimeException之外,一切直接承继 Exception 的反常都是查看反常。

  • 非查看反常:也称为“运转时反常” ,编译器不会查看运转时反常,在抛出运转时反常时编译器不会报错,当运转程序的时分才或许抛出该反常。Error及其子类和RuntimeException 及其子类都对错查看反常。

阐明:查看反常和非查看反常是针对编译器而言的,是编译器来查看该反常是否强制开发人员处理该反常:

  • 查看反常导致反常在办法调用链上显式传递,而且一旦底层接口的查看反常声明产生变化,会导致整个调用链代码更改。
  • 运用非查看反常不会影响办法签名,而且调用方能够自由决议何时何地捕获和处理反常

主张运用非查看反常让代码更加简练,而且更容易保持接口的稳定性。

查看反常举例

在代码中运用 throw 关键字手动抛出一个查看反常,编译器提示过错,如下图所示:

Java异常处理和最佳实践(含案例分析)

经过编译器提示,有两种办法处理查看反常,要么将反常增加到办法签名上,要么捕获反常:

Java异常处理和最佳实践(含案例分析)

办法一: 将反常增加到办法签名上,经过 throws 关键字抛出反常,由调用该办法的办法处理该反常:

Java异常处理和最佳实践(含案例分析)

办法二: 运用 try-catch 捕获反常,在 catch 代码块中处理该反常,下面的代码是将查看反常包装在非查看反常中从头抛出,这样编译器就不会提示过错了,关于怎么处理反常后边会具体介绍:

Java异常处理和最佳实践(含案例分析)

非查看反常举例

一切承继 RuntimeException 的反常都对错查看反常,直接抛出非查看反常编译器不会提示过错:

Java异常处理和最佳实践(含案例分析)

自界说查看反常

自界说查看反常只需求承继 Exception 即可,如下代码所示:

Java异常处理和最佳实践(含案例分析)

自界说查看反常的处理办法前面现已介绍,这儿不再赘述。

自界说非查看反常

自界说非查看反常只需求承继 RuntimeException 即可,如下代码所示:

Java异常处理和最佳实践(含案例分析)

2、从字节码层面剖析反常处理

前面现已简略介绍了一下Java 的反常体系,以及怎么自界说反常,下面我将从字节码层面剖析反常处理机制,经过字节码的剖析你将对 try-catch-finally 有更加深化的认识。

try-catch-finally的实质

首要查阅jvm 官方文档,有如下的描绘阐明:

Java异常处理和最佳实践(含案例分析)

从官方文档的描绘咱们能够知道,图片中的字节码是在 JDK 1.6 (class 文件的版别号为50,表明java编译器的版别为jdk 1.6)及之前的编译器生成的,由于有 jsr 和 ret 指令能够运用。然而在 idea 中经过 jclasslib 插件查看 try-catch-finally 的字节码文件并没有 jsr/ret 指令,经过查阅材料,有如下阐明:

jsr / ret 机制最初用于完成finally块,可是他们认为节约代码大小并不值得额外的复杂性,因此逐步被淘汰了。Sun JDK 1.6之后的javac就不生成jsr/ret指令了,那finally块要怎么完成?

javac采用的办法是把finally块的内容复制到原本每个jsr指令地点的当地,这样就不需求jsr/ret了,代价则是字节码大小会膨胀,可是降低了字节码的复杂性,由于减少了两个字节码指令(jsr/ret)。

事例一:try-catch 字节码剖析

在 JDK 1.8 中 try-catch 的字节码如下所示:

Java异常处理和最佳实践(含案例分析)

这儿需求阐明一下 athrow 指令的效果:

Java异常处理和最佳实践(含案例分析)

反常表

Java异常处理和最佳实践(含案例分析)

athrow指令: 在Java程序中显示抛出反常的操作(throw句子)都是由 athrow指令来完成的,athrow 指令抛出的Objectref 有必要是类型引证,而且有必要作为 Throwable 类或 Throwable 子类的实例目标。它从操作数仓库中弹出,然后经过在当时办法的反常表中搜索与 objectref 类匹配的榜首个反常处理程序:

  • 假如在反常表中找到与 objectref 匹配的反常处理程序,PC 寄存器被重置到用于处理此反常的代码的方位,然后会清除当时帧的操作数仓库,objectref 被推回操作数仓库,履行持续。
  • 假如在当时框架中没有找到匹配的反常处理程序,则弹出该栈帧,该反常会从头抛给上层调用的办法。假如当时帧表明同步办法的调用,那么在调用该办法时输入或从头输入的监督器将退出,就好像履行了监督退出指令(monitorexit)相同。
  • 假如在一切栈帧弹出前依然没有找到适宜的反常处理程序,这个线程将停止。

反常表: 反常表中用来记载程序计数器的方位和反常类型。如上图所示,表明的意思是:假如在 8 到 16 (不包括16)之间的指令抛出的反常匹配 MyCheckedException 类型的反常,那么程序跳转到16 的方位持续履行。

剖析上图中的字节码: 榜首个 athrow 指令抛出 MyCheckedException 反常到操作数栈顶,然后去到反常表中查找是否有对应的类型,反常表中有 MyCheckedException ,然后跳转到 16 持续履行代码。第二个 athrow 指令抛出 RuntimeException 反常,然后在反常表中没有找到匹配的类型,当时办法强制完毕并弹出当时栈帧,该反常从头抛给调用者,任然没有找到匹配的处理器,该线程被停止。

事例二:try-catch-finally 字节码剖析

在刚刚的代码根底之上增加 finally 代码块,然后剖析字节码如下:

Java异常处理和最佳实践(含案例分析)

反常表的信息如下:

Java异常处理和最佳实践(含案例分析)

增加 finally 代码块后,在反常表中新增了一条记载,捕获类型为 any,这儿解释一下这条记载的含义:

在 8 到 27(不包括27) 之间的指令履行进程中,抛出或许回来任何类型的成果都会跳转到 26 持续履行。

从上图的字节码中能够看到,字节码索引为 26 后到完毕的指令都是 finally 块中的代码,再解释一下finally块的字节码指令的含义,从 25 开端介绍,finally 块的代码是从 26 开端的:

25 athrow// 匹配到反常表中的反常 any,清空操作数栈,将 RuntimeExcepion 的引证增加到操作数栈顶,然后跳转到26 持续履行26 astore_2// 将栈顶的引证保存到局部变量表索引为 2 的方位27 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>// 获取类的静态字段引证放在操作数栈顶30 ldc #9 <履行finally 代码>//将字符串的放在操作数栈顶32 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>// 调用办法35 aload_2// 将局部变量表索引为 2 到引证放到操作数栈顶,这儿便是前面抛出的RuntimeExcepion 的引证36 athrow// 在反常表中没有找到对应的反常处理程序,弹出该栈帧,该反常会从头抛给上层调用的办法
事例三:finally 块中的代码为什么总是会履行

Java异常处理和最佳实践(含案例分析)

Java异常处理和最佳实践(含案例分析)

简略剖析一下上面代码的字节码指令:字节码指令 2 到 8 会抛出 ArithmeticException 反常,该反常是 Exception 的子类,正好匹配反常表中的榜首行记载,然后跳转到 13 持续履行,也便是履行 catch 块中的代码,然后履行 finally 块中的代码,最终经过 goto 31 跳转到 finally 块之外履行后续的代码。

假如 try 块中没有抛出反常,则履行完 try 块中的代码然后持续履行 finally 块中的代码,由于编译器在编译的时分将 finally 块中的代码增加到了 try 块代码后边,履行完 finally 的代码后经过 goto 31 跳转到 finally 块之外履行后续的代码 。

编译器会将 finally 块中的代码放在 try 块和 catch 块的末尾,所以 finally 块中的代码总是会履行。

经过上面的剖析,你应该能够知道 finally 块的代码为什么总是会履行了,假如仍是有不明白的当地欢迎留言评论。

事例四:finally 块中运用 return 字节码剖析
public int getInt() {
    int i = 0;
    try {
        i = 1;
        return i;
    } finally {
        i = 2;
        return i;
    }
}
public int getInt2() {
    int i = 0;
    try {
        i = 1;
        return i;
    } finally {
        i = 2;
    }
}

先剖析一下 getInt() 办法的字节码:

Java异常处理和最佳实践(含案例分析)

局部变量表:

Java异常处理和最佳实践(含案例分析)

反常表:

Java异常处理和最佳实践(含案例分析)

总结: 从上面的字节码中咱们能够看出,假如finally 块中有 return 关键字,那么 try 块以及 catch 块中的 return 都将会失效,所以在开发的进程中不应该在 finally 块中写 return 句子。

先剖析一下 getInt2() 办法的字节码:

Java异常处理和最佳实践(含案例分析)

反常表:

Java异常处理和最佳实践(含案例分析)

从上图字节码的剖析,咱们能够知道,虽然履行了finally块中的代码,可是回来的值仍是 1,这是由于在履行finally代码块之前,将本来局部变量表索引为 1 的值 1 保存到了局部变量表索引为 2 的方位,最终回来到是局部变量表索引为 2 的值,也便是本来的 1。

总结:假如在 finally 块中没有 return 句子,那么无论在 finally 代码块中是否修正回来值,回来值都不会改动,依然是履行 finally 代码块之前的值。

try-with-resources 的实质

下面经过一个打包文件的代码来演示阐明一下 try-with-resources 的实质:

 /**
     * 打包多个文件为 zip 格式
     *
     * @param fileList 文件列表
     */
    public static void zipFile(List<File> fileList) {
        // 文件的压缩包路径
        String zipPath = OUT + "/打包附件.zip";
        // 获取文件压缩包输出流
        try (OutputStream outputStream = new FileOutputStream(zipPath);
             CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
             ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream)) {
            for (File file : fileList) {
                // 获取文件输入流
                InputStream fileIn = new FileInputStream(file);
                // 运用 common.io中的IOUtils获取文件字节数组
                byte[] bytes = IOUtils.toByteArray(fileIn);
                // 写入数据并改写
                zipOut.putNextEntry(new ZipEntry(file.getName()));
                zipOut.write(bytes, 0, bytes.length);
                zipOut.flush();
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到");
        } catch (IOException e) {
            System.out.println("读取文件反常");
        }
    }

能够看到在 try() 的括号中界说需求封闭的资源,实际上这是Java的一种语法糖,查看编译后的代码就知道编译器为咱们做了什么,下面是反编译后的代码:

    public static void zipFile(List<File> fileList) {
        String zipPath = "./打包附件.zip";
        try {
            OutputStream outputStream = new FileOutputStream(zipPath);
            Throwable var3 = null;
            try {
                CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
                Throwable var5 = null;
                try {
                    ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream);
                    Throwable var7 = null;
                    try {
                        Iterator var8 = fileList.iterator();
                        while(var8.hasNext()) {
                            File file = (File)var8.next();
                            InputStream fileIn = new FileInputStream(file);
                            byte[] bytes = IOUtils.toByteArray(fileIn);
                            zipOut.putNextEntry(new ZipEntry(file.getName()));
                            zipOut.write(bytes, 0, bytes.length);
                            zipOut.flush();
                        }
                    } catch (Throwable var60) {
                        var7 = var60;
                        throw var60;
                    } finally {
                        if (zipOut != null) {
                            if (var7 != null) {
                                try {
                                    zipOut.close();
                                } catch (Throwable var59) {
                                    var7.addSuppressed(var59);
                                }
                            } else {
                                zipOut.close();
                            }
                        }
                    }
                } catch (Throwable var62) {
                    var5 = var62;
                    throw var62;
                } finally {
                    if (checkedOutputStream != null) {
                        if (var5 != null) {
                            try {
                                checkedOutputStream.close();
                            } catch (Throwable var58) {
                                var5.addSuppressed(var58);
                            }
                        } else {
                            checkedOutputStream.close();
                        }
                    }
                }
            } catch (Throwable var64) {
                var3 = var64;
                throw var64;
            } finally {
                if (outputStream != null) {
                    if (var3 != null) {
                        try {
                            outputStream.close();
                        } catch (Throwable var57) {
                            var3.addSuppressed(var57);
                        }
                    } else {
                        outputStream.close();
                    }
                }
            }
        } catch (FileNotFoundException var66) {
            System.out.println("文件未找到");
        } catch (IOException var67) {
            System.out.println("读取文件反常");
        }
    }

JDK1.7开端,java引入了 try-with-resources 声明,将 try-catch-finally 简化为 try-catch,在编译时会进行转化为 try-catch-finally 句子,咱们就不需求在 finally 块中手动封闭资源。

try-with-resources 声明包括三部分:try(声明需求封闭的资源)、try 块、catch 块。它要求在 try-with-resources 声明中界说的变量完成了 AutoCloseable 接口,这样在体系能够主动调用它们的close办法,然后替代了finally中封闭资源的功用,编译器为咱们生成的反常处理进程如下:

  • try 块没有产生反常时,主动调用 close 办法,
  • try 块产生反常,然后主动调用 close 办法,假如 close 也产生反常,catch 块只会捕捉 try 块抛出的反常,close 办法的反常会在catch 中经过调用 Throwable.addSuppressed 来限制反常,可是你能够在catch块中,用 Throwable.getSuppressed 办法来获取到限制反常的数组。

Java 反常处理不标准事例

反常处理分为三个阶段:捕获->传递->处理。try……catch的效果是捕获反常,throw的效果将反常传递给适宜的处理程序。捕获、传递、处理,三个阶段,任何一个阶段处理不妥,都会影响到整个体系。下面分别介绍一下常见的反常处理不标准事例。

捕获

  • 捕获反常的时分不区别反常类型
  • 捕获反常不完全,比方该捕获的反常类型没有捕获到
try{
    ……
} catch (Exception e){ // 不应对一切类型的反常统一捕获,应该笼统出事务反常和体系反常,分别捕获
    ……
}

传递

  • 反常信息丢掉
  • 反常信息转译过错,比方在抛出反常的时分将事务反常包装成了体系反常
  • 吃掉反常
  • 不必要的反常包装
  • 查看反常传递进程中不适用非查看检反常包装,造成代码被throws污染
try{
    ……
} catch (BIZException e){ 
    throw new BIZException(e); // 重复包装相同类型的反常信息 
} catch (Biz1Exception e){ 
    throw new BIZException(e.getMessage()); // 没有抛出反常栈信息,正确的做法是throw new BIZException(e); 
} catch (Biz2Exception e){
    throw new Exception(e); // 不能运用低笼统等级的反常去包装高笼统等级的反常,这样在传递进程中丢掉了反常类型信息
} catch (Biz3Exception e){
    throw new Exception(……); // 反常转译过错,将事务反常直接转译成了体系反常
} catch (Biz4Exception e){
    …… // 不抛出也不记Log,直接吃掉反常
} catch (Exception e){
    throw e;
}

处理

  • 重复处理
  • 处理办法不统一
  • 处理方位分散
try{
    try{
        try{
            ……
        } catch (Biz1Exception e){
            log.error(e);  // 重复的LOG记载
            throw new e;
        }
        try{
            ……
        } catch (Biz2Exception e){
            ……  // 相同是事务反常,既在内层处理,又在外层处理
        }
    } catch (BizException e){
        log.error(e); // 重复的LOG记载
        throw e;
    }
} catch (Exception e){
    // 通吃一切类型的反常
    log.error(e.getMessage(),e);
}

Java 反常处理标准事例

1、阿里巴巴Java反常处理规约

Java异常处理和最佳实践(含案例分析)

Java异常处理和最佳实践(含案例分析)

Java异常处理和最佳实践(含案例分析)

阿里巴巴Java开发标准中有15条反常处理的规约,其间下面两条运用的时分是比较困惑的,由于并没有告知咱们应该怎么界说反常,怎么抛出反常,怎么处理反常:

  • 【强制】捕获反常是为了处理它,不要捕获了却什么都不处理而抛弃之,假如不想处理它,请将该反常抛给它的调用者。最外层的事务运用者,有必要处理反常,将其转化为用户能够了解的内容。
  • 【引荐】界说时区别unchecked / checked 反常,避免直接运用RuntimeException抛出,更不答应抛出Exception或许Throwable,应运用有事务含义的自界说反常。

后边的章节我将依据自己的考虑,阐明怎么界说反常,怎么抛出反常,怎么处理反常,接着往下看。

2、反常处理最佳实践

1、运用 try-with-resource 封闭资源。

2、抛出具体的反常而不是 Exception,并在注释中运用 @throw 进行阐明。

3、捕获反常后运用描绘性言语记载过错信息,假如是调用外部服务最好是包括入参和出参。

logger.error("阐明信息,反常信息:{}", e.getMessage(), e)

4、优先捕获具体反常。

5、不要捕获 Throwable 反常,除非特殊状况。

6、不要忽略反常,反常捕获必定需求处理。

7、不要同时记载和抛出反常,由于反常会打印多次,正确的处理办法要么抛出反常要么记载反常,假如抛出反常,不要原封不动的抛出,能够自界说反常抛出。

8、自界说反常不要丢掉原有反常,应该将原始反常传入自界说反常中。

throw MyException("my exception", e);

9、自界说反常尽量不要运用查看反常。

10、尽或许晚的捕获反常,如非必要,主张一切的反常都不要在基层捕获,而应该由最上层捕获并统一处理这些反常。。

11、为了避免重复输出反常日志,主张一切的反常日志都统一交由最上层输出。就算基层捕获到了某个反常,如非特殊状况,也不要将反常信息输出,应该交给最上层统一输出日志。

项目中的反常处理实践

1、怎么自界说反常

在介绍怎么自界说反常之前,有必要阐明一下运用反常的长处,参阅Java反常的官方文档,总结有如下长处:

  • 能够将过错代码和正常代码别离
  • 能够在调用仓库上传递反常
  • 能够将反常分组和区别

在Java反常体系中界说了许多的反常,这些反常一般都是技术层面的反常,关于应用程序来说更多呈现的是事务相关的反常,比方用户输入了一些不合法的参数,用户没有登录等,咱们能够经过反常来对不同的事务问题进行分类,以便咱们排查问题,所以需求自界说反常。那咱们怎么自界说反常呢?前面现已说了,在应用程序中尽量不要界说查看反常,应该界说非查看反常(运转时反常)。

在我看来,应用程序中界说的反常应该分为两类:

  • 事务反常:用户能够看懂而且能够处理的反常,比方用户没有登录,提示用户登录即可。
  • 体系反常:用户看不懂需求程序员处理的反常,比方网络连接超时,需求程序员排查相关问题。

下面是我想象的关于应用程序中的反常体系分类:

Java异常处理和最佳实践(含案例分析)

在实在项目中,咱们一般在遇到不符合预期的状况下,经过抛出反常来阻挠程序持续运转,在抛出对应的反常时,需求在反常目标中描绘抛出该反常的原因以及反常仓库信息,以便提示用户和开发人员怎么处理该反常。

一般来说,反常的界说咱们能够参阅Java的其他反常界说就能够了,比方反常中有哪些结构办法,办法中有哪些结构参数,可是这样的自界说反常仅仅经过反常的类名对反常进行了一个分类,关于反常的描绘信息仍是不行完善,由于反常的描绘信息仅仅一个字符串。我觉得反常的描绘信息还应该包括一个过错码(code),反常中包括过错码的长处是什么呢?我能想到的便是和http恳求中的状况码的长处差不多,还有一点便是能够方便供给翻译功用,关于不同的言语环境能够经过过错码找到对应言语的过错提示信息而不需求修正代码。

根据上述的阐明,我认为应该这样来界说反常类,需求界说一个描绘反常信息的枚举类,关于一些通用的反常信息能够在枚举中界说,如下所示:

/**
 * 反常信息枚举类
 *
 */
public enum ErrorCode {
    /**
     * 体系反常
     */
    SYSTEM_ERROR("A000", "体系反常"),
    /**
     * 事务反常
     */
    BIZ_ERROR("B000", "事务反常"),
    /**
     * 没有权限
     */
    NO_PERMISSION("B001", "没有权限"),
    ;
    /**
     * 过错码
     */
    private String code;
    /**
     * 过错信息
     */
    private String message;
    ErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
    /**
     * 获取过错码
     *
     * @return 过错码
     */
    public String getCode() {
        return code;
    }
    /**
     * 获取过错信息
     *
     * @return 过错信息
     */
    public String getMessage() {
        return message;
    }
    /**
     * 设置过错码
     *
     * @param code 过错码
     * @return 回来当时枚举
     */
    public ErrorCode setCode(String code) {
        this.code = code;
        return this;
    }
    /**
     * 设置过错信息
     *
     * @param message 过错信息
     * @return 回来当时枚举
     */
    public ErrorCode setMessage(String message) {
        this.message = message;
        return this;
    }
}

自界说体系反常类,其他类型的反常类似,仅仅反常的类名不同,如下代码所示:

/**
 * 体系反常类
 *
 */
public class SystemException extends RuntimeException {
    private static final long serialVersionUID = 8312907182931723379L;
  /**
     * 过错码
     */
    private String code;
    /**
     * 结构一个没有过错信息的 <code>SystemException</code>
     */
    public SystemException() {
        super();
    }
    /**
     * 运用指定的 Throwable 和 Throwable.toString() 作为反常信息来结构 SystemException
     *
     * @param cause 过错原因, 经过 Throwable.getCause() 办法能够获取传入的 cause信息
     */
    public SystemException(Throwable cause) {
        super(cause);
    }
    /**
     * 运用过错信息 message 结构 SystemException
     *
     * @param message 过错信息
     */
    public SystemException(String message) {
        super(message);
    }
    /**
     * 运用过错码和过错信息结构 SystemException
     *
     * @param code    过错码
     * @param message 过错信息
     */
    public SystemException(String code, String message) {
        super(message);
        this.code = code;
    }
    /**
     * 运用过错信息和 Throwable 结构 SystemException
     *
     * @param message 过错信息
     * @param cause   过错原因
     */
    public SystemException(String message, Throwable cause) {
        super(message, cause);
    }
    /**
     * @param code    过错码
     * @param message 过错信息
     * @param cause   过错原因
     */
    public SystemException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
    /**
     * @param errorCode ErrorCode
     */
    public SystemException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.code = errorCode.getCode();
    }
    /**
     * @param errorCode ErrorCode
     * @param cause     过错原因
     */
    public SystemException(ErrorCode errorCode, Throwable cause) {
        super(errorCode.getMessage(), cause);
        this.code = errorCode.getCode();
    }
    /**
     * 获取过错码
     *
     * @return 过错码
     */
    public String getCode() {
        return code;
    }
}

上面界说的 SystemException 类中界说了许多的结构办法,我这儿仅仅给出一个示例,所以保存了不传入过错码的结构办法,主张保存不运用过错码的结构办法,能够提高代码的灵活性,由于过错码的标准也是一个值得评论的问题,关于怎么界说过错码在阿里巴巴开发标准手册中有介绍,这儿不再具体阐明。

2、怎么运用反常

前面介绍了怎么自界说反常,接下来介绍一下怎么运用反常,也便是什么时分抛出反常。反常其实能够看作办法的回来成果,当呈现非预期的状况时,就能够经过抛出反常来阻挠程序持续履行。比方希望用户有管理员权限才能删去某条记载,假如用户没有管理员权限,那么就能够抛出没有权限的反常阻挠程序持续履行并提示用户需求管理员权限才能操作。

抛出反常运用 throw 关键字,如下所示:

throw new BizException(ErrorCode.NO_PERMISSION);

什么时分抛出事务反常,什么时分抛出体系反常?

事务反常(bizException/bussessException) :用户操作事务时,提示出来的反常信息,这些信息能直接让用户能够持续下一步操作,或许换一个正确操作办法去运用,换句话便是用户能够自己能处理的。比方:“用户没有登录”,“没有权限操作”。

体系反常(SystemException) :用户操作事务时,提示体系程序的反常信息,这类的反常信息时用户看不懂的,需求告警通知程序员排查对应的问题,如 NullPointerException,IndexOfException。另一个状况便是接口对接时,参数的校验时提示出来的信息,如:短少ID,短少有必要的参数等,这类的信息关于客户来说也是看不懂的,也是处理不了的,所以我把这两类的过错应当统一归类于体系反常。

关于应该抛出事务反常仍是体系反常,一句话总结便是:该反常用户能否处理,假如用户能处理则抛出事务反常,假如用户不能处理需求程序员处理则抛出体系反常。

在调用第三方的 rpc 接口时,咱们应该怎么处理反常呢?首要咱们需求知道 rpc 接口抛出反常仍是回来的包括过错码的 Result 目标,关于 rpc 应该回来反常仍是过错码有许多的评论,关于这方面的内容能够查看相关文档,这个不是本文的要点,经过实际调查知道 rpc 的回来基本都是包括过错码的 Result 目标,所以这儿以回来过错码的状况进行阐明。首要需求明确 rpc 调用失利应该回来体系反常,所以咱们能够界说一个承继 SystemException 的 rpc 反常 RpcException,代码如下所示:

/**
 * rpc 反常类
 */
public class RpcException extends SystemException {
    private static final long serialVersionUID = -9152774952913597366L;
    /**
     * 结构一个没有过错信息的 <code>RpcException</code>
     */
    public RpcException() {
        super();
    }
    /**
     * 运用指定的 Throwable 和 Throwable.toString() 作为反常信息来结构 RpcException
     *
     * @param cause 过错原因, 经过 Throwable.getCause() 办法能够获取传入的 cause信息
     */
    public RpcException(Throwable cause) {
        super(cause);
    }
    /**
     * 运用过错信息 message 结构 RpcException
     *
     * @param message 过错信息
     */
    public RpcException(String message) {
        super(message);
    }
    /**
     * 运用过错码和过错信息结构 RpcException
     *
     * @param code    过错码
     * @param message 过错信息
     */
    public RpcException(String code, String message) {
        super(code, message);
    }
    /**
     * 运用过错信息和 Throwable 结构 RpcException
     *
     * @param message 过错信息
     * @param cause   过错原因
     */
    public RpcException(String message, Throwable cause) {
        super(message, cause);
    }
    /**
     * @param code    过错码
     * @param message 过错信息
     * @param cause   过错原因
     */
    public RpcException(String code, String message, Throwable cause) {
        super(code, message, cause);
    }
    /**
     * @param errorCode ErrorCode
     */
    public RpcException(ErrorCode errorCode) {
        super(errorCode);
    }
    /**
     * @param errorCode ErrorCode
     * @param cause     过错原因
     */
    public RpcException(ErrorCode errorCode, Throwable cause) {
        super(errorCode, cause);
    }
}

这个 RpcException 一切的结构办法都是调用的父类 SystemExcepion 的办法,所以这儿不再赘述。界说好了反常后接下来是处理 rpc 调用的反常处理逻辑,调用 rpc 服务或许会产生 ConnectException 等网络反常,咱们并不需求在调用的时分捕获反常,而是应该在最上层捕获并处理反常,调用 rpc 的处理demo代码如下:

private Object callRpc() {
    Result<Object> rpc = rpcDemo.rpc();
    log.info("调用第三方rpc回来成果为:{}", rpc);
    if (Objects.isNull(rpc)) {
        return null;
    }
    if (!rpc.getSuccess()) {
        throw new RpcException(ErrorCode.RPC_ERROR.setMessage(rpc.getMessage()));
    }
    return rpc.getData();
}

3、怎么处理反常

咱们应该尽或许晚的捕获反常,如非必要,主张一切的反常都不要在基层捕获,而应该由最上层捕获并统一处理这些反常。前面的现已简略阐明晰一下怎么处理反常,接下来将经过代码的办法解说怎么处理反常。

rpc 接口大局反常处理

关于 rpc 接口,咱们这儿将 rpc 接口的回来成果封装到包括过错码的 Result 目标中,所以能够界说一个 aop 叫做 RpcGlobalExceptionAop,在 rpc 接口履行前后捕获反常,并将捕获的反常信息封装到 Result 目标中回来给调用者。

Result 目标的界说如下:

/**
 * Result 成果类
 *
 */
public class Result<T> implements Serializable {
    private static final long serialVersionUID = -1525914055479353120L;
    /**
     * 过错码
     */
    private final String code;
    /**
     * 提示信息
     */
    private final String message;
    /**
     * 回来数据
     */
    private final T data;
    /**
     * 是否成功
     */
    private final Boolean success;
    /**
     * 结构办法
     *
     * @param code    过错码
     * @param message 提示信息
     * @param data    回来的数据
     * @param success 是否成功
     */
    public Result(String code, String message, T data, Boolean success) {
        this.code = code;
        this.message = message;
        this.data = data;
        this.success = success;
    }
    /**
     * 创建 Result 目标
     *
     * @param code    过错码
     * @param message 提示信息
     * @param data    回来的数据
     * @param success 是否成功
     */
    public static <T> Result<T> of(String code, String message, T data, Boolean success) {
        return new Result<>(code, message, data, success);
    }
    /**
     * 成功,没有回来数据
     *
     * @param <T> 范型参数
     * @return Result
     */
    public static <T> Result<T> success() {
        return of("00000", "成功", null, true);
    }
    /**
     * 成功,有回来数据
     *
     * @param data 回来数据
     * @param <T>  范型参数
     * @return Result
     */
    public static <T> Result<T> success(T data) {
        return of("00000", "成功", data, true);
    }
    /**
     * 失利,有过错信息
     *
     * @param message 过错信息
     * @param <T>     范型参数
     * @return Result
     */
    public static <T> Result<T> fail(String message) {
        return of("10000", message, null, false);
    }
    /**
     * 失利,有过错码和过错信息
     *
     * @param code    过错码
     * @param message 过错信息
     * @param <T>     范型参数
     * @return Result
     */
    public static <T> Result<T> fail(String code, String message) {
        return of(code, message, null, false);
    }
    /**
     * 获取过错码
     *
     * @return 过错码
     */
    public String getCode() {
        return code;
    }
    /**
     * 获取提示信息
     *
     * @return 提示信息
     */
    public String getMessage() {
        return message;
    }
    /**
     * 获取数据
     *
     * @return 回来的数据
     */
    public T getData() {
        return data;
    }
    /**
     * 获取是否成功
     *
     * @return 是否成功
     */
    public Boolean getSuccess() {
        return success;
    }
}

在编写 aop 代码之前需求先导入 spring-boot-starter-aop 依靠:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

RpcGlobalExceptionAop 代码如下:

/**
 * rpc 调用大局反常处理 aop 类
 *
 */
@Slf4j
@Aspect
@Component
public class RpcGlobalExceptionAop {
    /**
     * execution(* com.xyz.service ..*.*(..)):表明 rpc 接口完成类包中的一切办法
     */
    @Pointcut("execution(* com.xyz.service ..*.*(..))")
    public void pointcut() {}
    @Around(value = "pointcut()")
    public Object handleException(ProceedingJoinPoint joinPoint) {
        try {
            //假如对传入对参数有修正,那么需求调用joinPoint.proceed(Object[] args)
            //这儿没有修正参数,则调用joinPoint.proceed()办法即可
            return joinPoint.proceed();
        } catch (BizException e) {
            // 关于事务反常,应该记载 warn 日志即可,避免无效告警
            log.warn("大局捕获事务反常", e);
            return Result.fail(e.getCode(), e.getMessage());
        } catch (RpcException e) {
            log.error("大局捕获第三方rpc调用反常", e);
            return Result.fail(e.getCode(), e.getMessage());
        } catch (SystemException e) {
            log.error("大局捕获体系反常", e);
            return Result.fail(e.getCode(), e.getMessage());
        } catch (Throwable e) {
            log.error("大局捕获不知道反常", e);
            return Result.fail(e.getMessage());
        }
    }
}

aop 中 @Pointcut 的 execution 表达式配置阐明:

execution(public * *(..)) 界说恣意公共办法的履行
execution(* set*(..)) 界说任何一个以"set"开端的办法的履行
execution(* com.xyz.service.AccountService.*(..)) 界说AccountService 接口的恣意办法的履行
execution(* com.xyz.service.*.*(..)) 界说在service包里的恣意办法的履行
execution(* com.xyz.service ..*.*(..)) 界说在service包和一切子包里的恣意类的恣意办法的履行
execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…)) 界说在pointcutexp包和一切子包里的JoinPointObjP2类的恣意办法的履行

http 接口大局反常处理

假如是 springboot 项目,http 接口的反常处理首要分为三类:

  • 根据恳求转发的办法处理反常;
  • 根据反常处理器的办法处理反常;
  • 根据过滤器的办法处理反常。

根据恳求转发的办法: 真正的大局反常处理。

完成办法有:

  • BasicExceptionController

根据反常处理器的办法: 不是真正的大局反常处理,由于它处理不了过滤器等抛出的反常。

完成办法有:

  • @ExceptionHandler
  • @ControllerAdvice+@ExceptionHandler
  • SimpleMappingExceptionResolver
  • HandlerExceptionResolver

根据过滤器的办法 近似大局反常处理。它能处理过滤器及之后的环节抛出的反常。

完成办法有:

  • Filter

关于 http 接口的大局反常处理,这儿要点介绍根据反常处理器的办法,其他的办法主张查阅相关文档学习。

在介绍根据反常处理器的办法之前需求导入 spring-boot-starter-web 依靠即可,如下所示:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

经过@ControllerAdvice+@ExceptionHandler 完成根据反常处理器的http接口大局反常处理:

/**
* http 接口反常处理类
*/
@Slf4j
@RestControllerAdvice("org.example.controller")
public class HttpExceptionHandler {
    /**
     * 处理事务反常
     * @param request 恳求参数
     * @param e 反常
     * @return Result
     */
    @ExceptionHandler(value = BizException.class)
    public Object bizExceptionHandler(HttpServletRequest request, BizException e) {
        log.warn("事务反常:" + e.getMessage() , e);
        return Result.fail(e.getCode(), e.getMessage());
    }
    /**
     * 处理体系反常
     * @param request 恳求参数
     * @param e 反常
     * @return Result
     */
    @ExceptionHandler(value = SystemException.class)
    public Object systemExceptionHandler(HttpServletRequest request, SystemException e) {
        log.error("体系反常:" + e.getMessage() , e);
        return Result.fail(e.getCode(), e.getMessage());
    }
    /**
     * 处理不知道反常
     * @param request 恳求参数
     * @param e 反常
     * @return Result
     */
    @ExceptionHandler(value = Throwable.class)
    public Object unknownExceptionHandler(HttpServletRequest request, Throwable e) {
        log.error("不知道反常:" + e.getMessage() , e);
        return Result.fail(e.getMessage());
    }
}

在 HttpExceptionHandler 类中,@RestControllerAdvice = @ControllerAdvice + @ResponseBody ,假如有其他的反常需求处理,只需求界说@ExceptionHandler注解的办法处理即可。

总结

读完本文应该了解Java反常处理机制,当一个反常被抛出时,JVM会在当时的办法里寻觅一个匹配的处理,假如没有找到,这个办法会强制完毕并弹出当时栈帧,而且反常会从头抛给上层调用的办法(在调用办法帧)。假如在一切帧弹出前依然没有找到适宜的反常处理,这个线程将停止。假如这个反常在最终一个非守护线程里抛出,将会导致JVM自己停止,比方这个线程是个main线程。

最终对本文的内容做一个简略的总结,Java言语的反常处理办法有两种,一种是 try-catch 捕获反常,另一种是经过 throw 抛出反常。在程序中能够抛出两种类型的反常,一种是查看反常,另一种对错查看反常,应该尽量抛出非查看反常,遇到查看反常应该捕获进行处理不要抛给上层。在反常处理的时分应该尽或许晚的处理反常,最好是界说一个大局反常处理器,在大局反常处理器中处理一切抛出的反常,并将反常信息封装到 Result 目标中回来给调用者。

参阅文档:

javainsimpleway.com/exception-h…

www.infoq.com/presentatio…

docs.oracle.com/javase/tuto…

java 官方文档:docs.oracle.com/javase/spec…