关于阿里云:Java异常处理和最佳实践含案例分析

11次阅读

共计 19650 个字符,预计需要花费 50 分钟才能阅读完成。

作者:王迪(惜鸟)

概述

最近在代码 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 异样分类

总结:

  • Thorwable 类(示意可抛出)是所有异样和谬误的超类,两个间接子类为 Error 和 Exception,别离示意谬误和异样。
  • 其中异样类 Exception 又分为运行时异样 (RuntimeException) 和非运行时异样,这两种异样有很大的区别,也称之为非查看异样(Unchecked Exception)和查看异样(Checked Exception),其中 Error 类及其子类也是非查看异样。

查看异样和非查看异样

  • 查看异样:也称为“编译时异样”,编译器在编译期间查看的那些异样。因为编译器“查看”这些异样以确保它们失去解决,因而称为“查看异样”。如果抛出查看异样,那么编译器会报错,须要开发人员手动解决该异样,要么捕捉,要么从新抛出。除了 RuntimeException 之外,所有间接继承 Exception 的异样都是查看异样。
  • 非查看异样:也称为“运行时异样”,编译器不会查看运行时异样,在抛出运行时异样时编译器不会报错,当运行程序的时候才可能抛出该异样。Error 及其子类和 RuntimeException 及其子类都是非查看异样。

阐明:查看异样和非查看异样是针对编译器而言的,是编译器来查看该异样是否强制开发人员解决该异样:

  • 查看异样导致异样在办法调用链上显式传递,而且一旦底层接口的查看异样申明发生变化,会导致整个调用链代码更改。
  • 应用非查看异样不会影响办法签名,而且调用方能够自在决定何时何地捕捉和解决异样

倡议应用非查看异样让代码更加简洁,而且更容易放弃接口的稳定性。

查看异样举例

在代码中应用 throw 关键字手动抛出一个查看异样,编译器提醒谬误,如下图所示:

通过编译器提醒,有两种形式解决查看异样,要么将异样增加到办法签名上,要么捕捉异样:

形式一: 将异样增加到办法签名上,通过 throws 关键字抛出异样,由调用该办法的办法解决该异样:

形式二: 应用 try-catch 捕捉异样,在 catch 代码块中解决该异样,上面的代码是将查看异样包装在非查看异样中从新抛出,这样编译器就不会提醒谬误了,对于如何解决异样前面会具体介绍:

非查看异样举例

所有继承 RuntimeException 的异样都是非查看异样,间接抛出非查看异样编译器不会提醒谬误:

自定义查看异样

自定义查看异样只须要 继承 Exception 即可,如下代码所示:

自定义查看异样的解决形式后面曾经介绍,这里不再赘述。

自定义非查看异样

自定义非查看异样只须要 继承 RuntimeException 即可,如下代码所示:

2、从字节码层面剖析异样解决

后面曾经简略介绍了一下 Java 的异样体系,以及如何自定义异样,上面我将从字节码层面剖析异样解决机制,通过字节码的剖析你将对 try-catch-finally 有更加深刻的意识。

try-catch-finally 的实质

首先查阅 jvm 官网文档,有如下的形容阐明:

从官网文档的形容咱们能够晓得,图片中的字节码是在 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 的字节码如下所示:

这里须要阐明一下 athrow 指令的作用:

异样表

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 代码块,而后剖析字节码如下:

异样表的信息如下:

增加 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 块中的代码为什么总是会执行

简略剖析一下下面代码的字节码指令:字节码指令 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() 办法的字节码:

局部变量表:

异样表:

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

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

异样表:

从上图字节码的剖析,咱们能够晓得,尽管执行了 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 开发标准中有 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 的其余异样定义就能够了,比方异样中有哪些构造方法,办法中有哪些结构参数,然而这样的自定义异样只是通过异样的类名对异样进行了一个分类,对于异样的形容信息还是不够欠缺,因为异样的形容信息只是一个字符串。我感觉异样的形容信息还应该蕴含一个错误码(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 对象中返回给调用者。

参考文档:

http://javainsimpleway.com/exception-handling-best-practices/

https://www.infoq.com/presentations/effective-api-design/

https://docs.oracle.com/javase/tutorial/essential/exceptions/…

java 官网文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.htm…

正文完
 0