Java异常最佳实践

Java异常最佳实践

PS:本文章深入分析Java的 异常体系自定义异常、会分享异常处理的最佳实践,如有错误还希望同学们直接指出。

内容借鉴张洪亮老师的 《深入理解Java核心核心技术》 - 第九章:异常


导航

  • Java异常最佳实践
    • 一、异常体系
      • 1、Error
      • 2、Exception
        • 2.1 Checked Exception (受检异常)
        • 2.2 Unchecked Exception (非受检异常)
          • 不捕获异常
          • 捕获异常
    • 二、异常关键字
      • 1、声明异常
      • 2、抛出异常
      • 3、捕获异常
    • 三、异常链
    • 四、异常处理最佳实践
      • 1、不要使用异常来控制业务逻辑
      • 2、如果处理不了,请不要捕获
      • 3、在catch语句中不要使用printStackTrace()
      • 4、二次抛出异常时、要带上异常链
      • 5、在需要的地方声明特定的受检异常
      • 6、异常捕获的顺序需要特殊注意
      • 7、可以直接捕获Exception,但是要注意场景
      • 8、可以直接捕获Throwable,但是要注意场景
      • 9、不要在finally中抛出异常
      • 10、在finally中释放资源
      • 11、如果不想处理异常,则使用finally块而不是catch块
      • 12、善于使用自定义异常
      • 13、try块中的代码要尽可能的少
    • 五、自定义异常实践(干货)
    • 六、总结

一、异常体系

Java程序在运行过程中(不止Java),会出现各种各样的超出正常范围的情况,这种情况我们称之为 程序异常, 为了在程序出现异常时能够清晰的表示出这些 “特殊情况” ,Java定义了一套非常完善的异常体系。

Java的异常体系以及所有的异常都是是从Throwable延伸出来的。Throwable继承自Object类,是所有异常的顶级父类,它下面又分为两个重要的子类:

  • Error:表示错误
  • Exception:表示异常

下面分别介绍它们两个


1、Error

在程序运行期间,Java将有可能发生的 “特殊情况” 分为了两大类,一种是程序员可以处理(或者说可以预见的)情况,一种是程序员无法处理的情况

我们无法处理的问题称之为Error,可以处理的问题称之为Exception

Eroor表示系统级别的异常,是Java运行环境的内部错误或者硬件问题,从程序方面无法解决这样的情况,程序除了down掉别无他选,比如我们常见的OutOfMemoryError(堆溢出)、StackOverflowError(栈溢出),NoClassDefFoundError(类未定义)、NoSuchMethodError(找不到方法)等,当程序运行期间碰到上述但不仅限于这些问题时,JVM唯一的选择就是退出运行。


2、Exception

Java中的Exception表示异常,就是我们可以预见并处理的那一类,出现异常问题时,通常是因为程序代码设计不完善、不合理导致的。

Exception又可以分为两大类:即是Checked Exception(受检异常)和Unchecked Exception(非受检异常)。

简单的说:受检异常会强制要求我们在代码中处理捕获,如果不捕获则代码无法通过编译期。而非受检异常不要求我们必须处理,即使不处理代码也能正常通过编译。

2.1 Checked Exception (受检异常)

受检异常是一种明确的异常,用于提醒我们必须对这种异常进行处理(我们可以理解为:它是很容易发生的、可以预见的异常),例如FileNotFoundException,当我们调用相关方法抛出了明确的异常,这是强制性的要求程序员对这种情况进行显式处理。

所以,当我们希望自己写的某个方法的调用者明确处理一些特殊情况时,就应该使用受检异常。

2.2 Unchecked Exception (非受检异常)

非受检异常一般是程序运行时异常,继承自RunTimeException,在编写代码时,不需要显式的处理、捕获非受检异常。但是如果不捕获,在程序运行期间出现异常就会中断(注意是从抛出异常的行数后中断运行,不是程序停止运行!)程序代码的执行。

运行时异常一般都是代码原因导致,例如上面例子中的NullPointerException(空指针异常)、IndexOutOfBoundsException(数组下标越界)、NumberFormatException(数字转换异常)等等。

下面贴出代码分别对不捕获、捕获两种情况进行说明。(ps:不了解try catch的同学可以不看代码,后面会对try catch进行说明)

不捕获异常
String s1 = null;
String s2 = "";
System.out.println(s1.equals(s2)); // 故意使用null.equals() ,将出现 NullPointerException 空指针异常
System.out.println("程序正常执行"); // 出现异常后,程序将会中断,后续代码将不会执行
System.out.println("程序运行结束"); 

运行结果:

java.lang.NullPointerException
	at com.study.throwable.ThrowableTest.main(ThrowableTest.java:27)
捕获异常

说明:注意是try中的代码块从抛出异常的代码行数后中断运行,不是程序停止运行,try块外且在try块后面的代码会继续运行的! 后面的文章中也会说明try代码块的用法以及应当遵循的规范。

String s1 = null;
String s2 = "";
try {
    System.out.println(s1.equals(s2)); 
    System.out.println("程序正常执行"); //抛出异常后,try块将中断,所以此行代码不会执行
} catch (RuntimeException e) {
    e.printStackTrace(); // 打印异常栈信息到控制台
}
System.out.println("程序运行结束"); //因为捕获了异常,所以此行代码正常执行

运行结果:

java.lang.NullPointerException
	at com.study.throwable.ThrowableTest.main(ThrowableTest.java:27)
程序运行结束  // 可以看到,最后一行代码依然正常执行

二、异常关键字

在了解Java的异常体系后,接下来我们了解如何使用和处理异常

Java的异常处理主要包括声明异常、抛出异常、捕获异常处理异常等几个过程,下面我们重点介绍前三种

1、声明异常

在Java中,想要声明某一个方法可能抛出的异常信息时,需要用到throws关键字,通过使用throws,表示异常被声明,但是并不处理。

//使用throws声明异常
public vouid method() throws Exception {
    //方法体
}

代码中通过throws声明了method可能抛出Exception异常,需要通过这个方法的调用者处理这个异常。

2、抛出异常

throws只是用于声明一个异常,声明一个方法可能抛出的异常,那如何明确、正确的抛出一个异常呢?这涉及到另外一个关键字,throw,在方法体中,想要明确的抛出一个异常时,可以使用throw

//使用throws声明异常
public void method() throws Exception {
    //使用throw手动抛出异常
    throw new Exception();
}

在异常抛出后吗,需要处理它,Java的处理方式分为两种

  • 自己处理
  • 继续向上抛,交给调用处理

对于继续向上抛这种处理方式,一般根据异常类型有不同的方式,如果是受检异常,则需要明确地再次声明异常,而非受检异常则不需要,例如:

public void caller() throws Exception {
    method();
}

public void method() throws Exception {
    throw new Exception();
}

caller()中调用了method()方法,而method()明确声明了一个受检异常Exception,那么对于调用者caller()来讲,如果无法处理就需要继续向上抛,这就需要在caller()上同样使用throws来声明受检异常。

3、捕获异常

我们说异常的处理方式要么是继续向上抛,要么是调用者自己处理,而自己处理就需要先捕获异常,才能进行处理

Java中,捕获异常需要用到try、catch、finally关键字。

  • try:用来指定一段需要预防出现异常情况的代码
  • catch:紧跟在try后,用来指定想要捕获的异常类型。
  • finally:为确保一段代码不管发生什么异常状况都要被执行
//try...catch...finally
try {
    //代码块
} catch (异常类型 异常对象) {
    //异常处理
} finally {
    //一定会执行的代码
}

//try-catch
try {
    //代码块
} catch (异常类型 异常对象) {
    //异常处理
}

//try-finally
try {
    //代码块
} finally {
    //一定会执行的代码
}

其中,try、catch、finally它们三个可以一起使用,catchfinally也可以只搭配try两两使用。另外不管如何搭配,try、finally只能有一个,而catch可以有多个,即在一次异常捕获过程中,可以同时对多个异常类型进行捕获:例如:

try {
    //代码块
} catch (Exception e1) {
    //异常处理
} catch (Exception e2) {
    //异常处理
} catch (Exception e3) {
    //异常处理
}

以上介绍了异常的声明、抛出以及捕获的方式,后面将介绍一些关于异常处理的最佳实践。

其实异常处理总结出来就一句话:自己明确知道如何处理的,就要处理;不知道如何处理的,就抛出,交给调用者去处理。


三、异常链

异常链是一种面向对象编程的技术,是指将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式(原异常被保存为新异常的一个属性),也就是说,一个方法应该抛出定义在相同抽象层次上的异常,但不会丢失更低层次的信息,有了异常链,我们就能知道异常发生的整个过程。

为了支持异常链的传递,JavaThrowable类中定义了以下几个构造方法:

public Throwable(String message, Throwable cause);
public Throwable(Throwable cause);

也就是说,我们在创建新的异常时,可以把已经发生的异常当做一个参数传递给新的异常,这样就构成了一个异常链。

其实很形象,就比如:异常B是由异常A引起的,我们如何表示?

try {
    //代码块
} catch (Exception b) {
    throw new Exception(a);
} 

这样我们在处理Exception b时,就能够知道这个异常是由Exception a引起的,更加方便使我们排查问题。


四、异常处理最佳实践

1、不要使用异常来控制业务逻辑

在开发具体业务时,很多人会使用异常来控制业务逻辑,例如:

try {
    execute();
} catch (Exception e) {
    execute1();
} catch (Exception1 e1) {
    execute2();
} catch (Exception2 e2) {
    execute3();
}

代码中的分支逻辑应该使用if else来控制,而不是依赖异常。使用异常来控制代码逻辑不容易理解,难于维护

2、如果处理不了,请不要捕获

很多人在开发中,遇到try块,后面一定会跟一个catch块,这是不对的。我们在开发中,应该只捕获那些能处理的异常,如果处理不了,就不要捕获它,继续向上抛出,谁能解决谁来捕获。

像下面这段代码对于异常处理就毫无意义:

try{
    //代码块
} catch (Exception e) {
    throw e;
}

甚至还有如下处理方式:

try{
    //代码块
} catch (Exception e) {
    //这是一个异常,请忽略它
}

在代码中捕获了异常,然后什么都没做,最可气的还写了一行无用的注释。

3、在catch语句中不要使用printStackTrace()

现在的开发工具都比较智能,在我们写一段try-catch代码时,通常会自动生成printStackTrace()语句,例如:

try {
    execute();
} catch (Exception e) {
    e.printStackTrace();
}

这段代码需要注意两个点:第一是e.printStackTrace();并不是处理异常,只是把异常堆栈信息打印到控制台,很多人认为打印异常就是处理异常。第二是实际开发中,我们的日志都应该打印到日志文件中,错误的日志更要通过log.error()类似的方式来进行输出。

4、二次抛出异常时、要带上异常链

我们在处理异常时,有时可能会先捕获一个异常,然后手动抛出另一个异常,这种情况我们一定要使用异常链,把捕获的异常也带上,避免丢失异常堆栈信息,异常链的用处上面已经阐述过。

try {
    //代码块
} catch (Exception e) {
    //错误用法
    throw new Exception();
    //正确用法
    throw new Exception(e, "xxxx");
}

5、在需要的地方声明特定的受检异常

善用受检异常,它的最大特点就是调用者必须明确处理这个异常,这是一种强制性的约束。所以当你的代码中有一些特殊情况或者重要情况需要让调用者做出处理时,做出关注时,就要使用受检异常,起到提醒的作用。

6、异常捕获的顺序需要特殊注意

很多人知道要处理异常,并且尝试在代码中捕获异常,因为可能有很多类型的异常抛出,所以会同时捕获多个异常,根据不同的异常做出不同的处理,于是就可能会是这样

try {
    //代码块
} catch (Exception e) {
    //处理1
} catch (MyException e) {
    //处理2
} catch (NullPointException e) {
    //处理3
}

以上的处理方式存在严重的问题,就是异常的捕获顺序不合理,以上形式的捕获异常,后面的MyException、NullPointException永远不会被捕获,异常一旦发生就会被Exception直接捕获了。

所以,在捕获异常时,要把范围较小的异常放在前面,也就是从子类 - > 父类的顺序,比如对于RuntimeExceptionExceptionThrowable的捕获一定要放在最后。

try {
    //代码块
} catch (NullPointException e) {
    //处理1
} catch (MyException e) {
    //处理2
} catch (Exception e) {
    //处理3
}

7、可以直接捕获Exception,但是要注意场景

在关于异常的处理上,很多人不建议直接对Exception、Throwable进行捕获,因为捕获的范围太大了,会导致永远无法知道异常的具体细节。

其实我们有时候可能还真的需要对Exception,甚至对Throwable进行捕获,尤其是现在很多应用的分布式、微服务化了,经常会有各种RPC接口的互相调用。我们在对外部提供一个RPC接口时,应该通过错误码的形式传递错误信息,而不是把异常抛给调用方,因为A系统的异常抛给B系统,B系统是一定处理不了的。

所以,我们往往需要在RPC接口中对Exception进行捕获,避免异常交给外部系统。

8、可以直接捕获Throwable,但是要注意场景

ThrowableErrorException两个子类,通常我们认为Error是程序员处理不了的。所以不建议捕获。

但是有一种特殊情况,我们可能需要捕获Error

当我们提供RPC服务时,一旦服务被调用过程中发生了Error,如NoSuchMethodError,我们没有捕获,那么这个错误就会一直向上抛,最终被RPC框架捕获。

RPC框架捕获这个异常后,可能会把错误日志打印到它自己的日志文件中,而不是我们应用的业务日志中。通常RPC框架自己的日志会有很多各种各样的超时等异常,我们很少对其进行错误监控,这就可能导致错误发生了,但我们无法察觉

9、不要在finally中抛出异常

示例代码:

try {
    execute();
} finally {
    throw new Exception();
}

execute()抛出异常后,我们在finally中再次抛出了一个异常,这就导致execute()方法抛出的异常信息完全丢失了,如果有catch块捕捉了异常也会被覆盖掉,丢失了异常链,会给后期问题的排查带来很大的困难。

10、在finally中释放资源

当我们想要释放一些资源时,如数据库链接,文件链接等,需要在finally中进行释放,因为finally在程序没有down掉的情况上来讲,一定会执行。

11、如果不想处理异常,则使用finally块而不是catch块

因为我们要在finally中释放资源,所以很对开发者会顺手把try-catch-finally都写上,这其实是错误的,当我们不想处理一个异常,又想在异常发生后做一些事情的时候,不要写catch块,而是使用finally块。

12、善于使用自定义异常

我们在日常开发中会接触很多异常,JDK内置了很多异常,一些框架中也会自定义各种各样的异常,我们自己也可以定义一些业务异常,这些异常可以有一定的继承关系,方便我们快速识别异常的原因,以及快速修复,比如OrderCanceledException、LoginFailedException等,我们通过这些异常的名字就知道具体发生了什么。

13、try块中的代码要尽可能的少

我维护过的个别项目中,很常见一个try块包着几百行代码,我们一定不能这样,要尽量控制try的粒度,不要无脑用try块包裹,这样会显的很不专业,对于那些明显不会异常的代码,何必要放在try块中呢?


五、自定义异常实践(干货)

基于Java异常体系,下面介绍一种在实际开发中比较好的实践,主要用到了自定义枚举异常、自定义异常、自定义错误码,自定义断言等。

1、首先定义一个接口,表示一种具有解释性的错误码,提供两个方法,用于返回错误码和错误描述信息

/**
 * 错误码方法定义接口
 *
 * @author baijiechong
 * @since 2023/4/24 21:58
 **/
public interface ExplicableErrorCode {

    /**
     * 获取描述信息
     *
     * @return 返回
     */
    String getMsg();

    /**
     * 返回错误码
     *
     * @return 返回
     */
    String getCode();

}

2、基于这个接口,我们新建一个错误码枚举类,定义一个具体的错误码信息,比如贷款管理业务去定义贷款管理相关的错误码

/**
 * 贷款相关错误码定义
 *
 * @author baijiechong
 * @since 2023/4/24 22:00
 **/
public enum LoanManageErrorCode implements ExplicableErrorCode {

    /**
     * 还款本金金额大于剩余本金金额
     */
    REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL("repay principal (%s) is greater than rest principal (%s)"),

    /**
     * 剩余本金为负
     */
    REST_PRINCIPAL_IS_NEGATIVE("rest principal (%s) is negative"),

    /**
     * 其他业务异常,不再一一列举
     */
    OTHER_EXCEPTION("other loan manage error");

    LoanManageErrorCode(String msg) {
        this.msg = msg;
    }

    private final String msg;

    @Override
    public String getMsg() {
        return msg;
    }

    @Override
    public String getCode() {
        return this.name();
    }
}

3、接下来,我们定义一个通用异常,异常中有一个成员变量,就是我们上一步定义的枚举错误码实例ExplicableErrorCode类型,实际指向LoanManageErrorCode这个枚举类(当然,如果有其他业务模块的异常,我们需要再新建枚举,同样去继承ExplicableErrorCode接口)

package com.study.throwable.exception;

import java.util.StringJoiner;

/**
 * 通用异常类
 *
 * 

* 系统所有自定义异常类都要继承自此异常类,此类是所有自定义异常的超类 *

注意:尽管不需要Throwable cause就可以构造异常,但是为了使抛出的异常信息更加清晰明了,请尽量使用带有Throwable cause 的构造方法

*

* * @author baijiechong * @since 2023/4/24 22:07 **/
public class BaseException extends RuntimeException { /** * 枚举错误码 */ public ExplicableErrorCode errorCode; /** * 带入异常说明中的参数 */ public Object[] args; /** * 自定义异常信息魔法值 */ private String detailMessage; public BaseException() { } /** * 构造1 - 不建议使用 * * @param detailMessage 自定义魔法值异常说明 */ public BaseException(String detailMessage) { super(detailMessage); this.detailMessage = detailMessage; } /** * 构造2 -不建议使用 * * @param detailMessage 自定义魔法值异常说明 * @param cause 异常链 */ public BaseException(String detailMessage, Throwable cause) { super(detailMessage, cause); this.detailMessage = detailMessage; } /** * 构造3 - 建议使用 * * @param errorCode 枚举错误码 */ public BaseException(ExplicableErrorCode errorCode) { super(errorCode.getMsg()); this.errorCode = errorCode; } /** * 构造4 - 建议使用 * * @param errorCode 枚举错误码 * @param cause 异常链 */ public BaseException(ExplicableErrorCode errorCode, Throwable cause) { super(errorCode.getMsg(), cause); this.errorCode = errorCode; } /** * 构造5 - 建议使用 * * @param errorCode 枚举错误码 * @param args 带入异常说明中的参数 */ public BaseException(ExplicableErrorCode errorCode, Object... args) { super(errorCode.getMsg()); this.args = args; this.errorCode = errorCode; } /** * 构造6 - 建议使用 * * @param errorCode 枚举错误码 * @param cause 异常链 * @param args 带入异常说明中的参数 */ public BaseException(ExplicableErrorCode errorCode, Throwable cause, Object... args) { super(errorCode.getMsg(), cause); this.args = args; this.errorCode = errorCode; } public String getErrorCode() { if (errorCode == null) { return ""; } return errorCode.getCode(); } public String getErrorMsg() { if (errorCode == null) { return detailMessage; } return String.format(errorCode.getMsg(), this.args); } public String getMessage() { return this.toString(); } @Override public String toString() { if (errorCode != null) { return new StringJoiner(" , ") .add("ErrorCode=[" + errorCode + "]") .add("Msg=[" + String.format(errorCode.getMsg(), this.args) + "]").toString(); } else { return detailMessage; } } }

这个toString()方法将异常中的错误码以及错误信息都打印出来了,实际开发中可以根据需要自定义,这个灵活度还是很高的,但是要遵循一条:简短扼要

4、基于通用异常BaseException.class, 我们就可以定义更多的自定义异常,如针对贷款业务管理相关的异常

/**
 * 资产管理相关异常
 *
 * 

注意:自定义的业务异常中,所有构造函数中必须调用父类BaseException.class的构造并传入相同的构造参数!

* * @author baijiechong * @since 2023/4/24 22:15 **/
public class LoanManageException extends BaseException { public LoanManageException() { super(); } public LoanManageException(String message) { super(message); } public LoanManageException(String message, Throwable cause) { super(message, cause); } public LoanManageException(ExplicableErrorCode errorCode) { super(errorCode); } public LoanManageException(ExplicableErrorCode errorCode, Throwable cause) { super(errorCode, cause); } public LoanManageException(ExplicableErrorCode errorCode, Object... args) { super(errorCode, args); } public LoanManageException(ExplicableErrorCode errorCode, Throwable cause, Object... args) { super(errorCode, cause, args); } }

这样我们在代码中就可以用以下多种方式抛出一个自定义异常(方式多种多样,大家可以举一反三):

throw new LoanManageException("抛异常啦");    //自定义异常信息
throw new LoanManageException("抛异常啦", e); //自定义异常信息 + 异常链
throw new LoanManageException(LoanManageErrorCode.OTHER_EXCEPTION);    //枚举错误码
throw new LoanManageException(LoanManageErrorCode.OTHER_EXCEPTION, e); //枚举错误码 + 异常链
//枚举错误码 + 需要带入异常信息的参数
throw new LoanManageException(LoanManageErrorCode.REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL, 100, 200); 
//枚举错误码 + 异常链 + 需要带入异常信息的参数
throw new LoanManageException(LoanManageErrorCode.REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL, e, 100, 200);

如果需要使用try-catch捕获异常链的话,请一定加上异常链,如下:

try {
    //代码块
} catch (Exception e) {
    throw new LoanManageException(LoanManageErrorCode.OTHER_EXCEPTION, e);//Exception e
}

当然这样做就违背了我们 特地检查数据异常的初衷了,因为不论是 还款本金金额大于剩余本金金额 或者 剩余本金为负 都不会自动被catch捕捉,所以这种写法应该用在除了特殊检查场景外,对于非自定义异常的捕获,相对来说更自由。

当异常被捕获后,就会向日志中打印:

ErrorCode=[REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL] , Msg=[repay principal (100) is greater than rest principal (200)]

这样更加方便我们排查问题。

为了方便我们使用自定义异常,还可以对其进行更深层次的封装,例如:

/**
 * 断言LoanManageErrorCode
 * 

* 断言类不需要也不应该加Throwable cause的异常链构造 *

* * @author baijiechong * @since 2023/4/24 22:24 **/
public class LoanManageAssert { public static void isTrue(boolean expression, LoanManageErrorCode errorCode, Object... args) { if (!expression) { throw new LoanManageException(errorCode, args); } } public static void isEquals(Integer num1, Integer num2, LoanManageErrorCode errorCode) { if (num1.compareTo(num2) != 0) { throw new LoanManageException(errorCode, num1, num2); } } public static void isLessThanOrEqualTo(Integer num1, Integer num2, LoanManageErrorCode errorCode) { if (num1.compareTo(num2) <= 0) { throw new LoanManageException(errorCode, num1, num2); } } }

我们定义了一个LoanManageAssert类,这个类中自定义了一系列断言方法,比如判断表达式结果是否为true,判断两个数是否相等。

当断言失败直接抛出LoanManageException,有了此类,我们在编写代码时就非常方便了,例如:

LoanModel loanModel = new LoanModel();
loanModel.setNum1(10);
loanModel.setNum2(14);
//使用断言类,将if else操作封装起来
LoanManageAssert.isLessThanOrEqualTo(
    loanModel.getNum1(), 
    loanModel.getNum2(), 
    LoanManageErrorCode.REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL);

这样就可以大大减少if else的无效代码,并且方便开发,提高代码复用

只需要通过断言工具类,对我们想要强校验的地方进行断言处理即可,一旦断言失败,就会抛出固定的LoanManageException,并且错误码使我们自己指定的。

5、当以上的步骤都完成后,系统中会存在许多不同业务类型相关的自定义异常,当这些异常在业务代码中大量的抛出时,我们应该如何合适的处理这些异常?一般会定义一个统一异常处理类,这里我利用spring aop来实现,将所有抛出的异常都打印进日志文件,没学过spring aop的小伙伴不懂也没事,总结一句话,就是声明一个类,来处理所有抛出的自定义异常以及Exception甚至Throwable。(这里对于aop 相关知识不再概述,后期我会出aop相关的文章)

先定义一个自定义注解@Exceptions,用于标注在方法上,声明此方法可能会抛出异常(包括自定义异常), 利用此注解实现aop对类方法的增强。

/**
 * 系统错误注解
 *
 * @author baijiechong
 * @since 2022/12/26 11:52
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Exceptions {

    /**
     * 异常方法说明,可以为空
     */
    String value() default "未知操作!";

}

定义切面类、切点以及增强逻辑,定义环绕增强来统一处理系统异常,实际的开发中可能除了异常的捕获、处理还会有其他一些操作,例如鉴权之类的。

/**
 * 系统日志,切面处理类
 *
 * @author baijiechong
 * @since 2022/12/26 11:52
 */
@Aspect
@Component
@AllArgsConstructor
public class ExceptionsAspect {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionsAspect.class);

    /**
     * 切入点,标注了@Exceptions的方法都会作为切入点
     */
    @Pointcut(value = "@annotation(com.study.throwable.exception.annotation.Exceptions)")
    public void exceptionPointCut() {
    }

    /**
     * 定义环绕通知,来处理系统异常
     */
    @Around("exceptionPointCut()")
    public Object surround(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Exceptions annotation = signature.getMethod().getAnnotation(Exceptions.class);
        logger.info("Before method = {} , methodDisplay = {} , params= {}",
                pjp.getSignature(),
                annotation.value(),
                JSON.toJSONString(pjp.getArgs(), SerializerFeature.IgnoreErrorGetter));
        Object proceed = null;
        try {
            //执行方法
            proceed = pjp.proceed(); 
        } catch (LoanManageException e) {
            //捕获到贷款业务相关异常,并输出到日志,其他的业务模块自定义异常可以写在LoanManageException后,依次排开
            logger.error("LoanManageException:{}, {}", e.getErrorCode(), e.getErrorMsg(), e);
        } catch (BaseException e) {
            //捕获到系统自定义异常,并输出到日志
            logger.error("BaseException:{}, {}", e.getErrorCode(), e.getErrorMsg(), e);
        } catch (Exception e) {
            logger.error("Exception: ", e);
        } catch (Throwable e) {
            logger.error("Throwable: ", e);
        } finally {
            logger.info("After method = {} , methodDisplay = {} , params= {} , result= {}",
                    pjp.getSignature(),
                    annotation.value(),
                    JSON.toJSONString(pjp.getArgs(), SerializerFeature.IgnoreErrorGetter),
                    proceed);
        }
        return proceed;
    }
}

到这里通用配置基本结束,接下来我们写一个service逻辑层,并声明一个获取贷款金额的方法来模拟业务异常,并查看异常处理结果

新建service服务接口

/**
 * @author baijiechong
 * @since 2023/4/25 21:34
 **/
public interface ILoanManagerService {

    /**
     * 获取贷款金额
     * 

* 测试方法,所以不带参数不带返回值 *

*/
public void getTheLoanAmount(); }

新建serviceImpl,基于ILoanManagerService的服务实现类

/**
 * @author baijiechong
 * @since 2023/4/25 21:33
 **/
@Service
public class LoanManagerServiceImpl implements ILoanManagerService {

    @Override
    @Exceptions(value = "获取贷款金额") //使用自定义注解,此类、此方法将会被aop动态代理增强,走我们统一处理异常逻辑的代码
    public void getTheLoanAmount() {
        LoanModel loanModel = new LoanModel();
        loanModel.setNum1(10);
        loanModel.setNum2(14);
        //使用断言类,将if else操作封装起来,值如果不符合预期,直接抛出异常
        LoanManageAssert.isLessThanOrEqualTo(
            loanModel.getNum1(), 
            loanModel.getNum2(), 
            LoanManageErrorCode.REPAY_PRINCIPAL_IS_GREATER_THAN_PRINCIPAL);
    }

}

编写Junit测试单元,调用getTheLoanAmount()方法,查看正常、异常两种情况

/**
 * @author baijiechong
 * @since 2023/4/24 21:14
 **/
@SpringBootTest
@RunWith(SpringRunner.class)
public class ThrowableTest {

    @Resource
    private ILoanManagerService loanManagerService;

    /**
     * 测试aop全局处理异常 以及自定义异常的最佳实践
     */
    @Test
    public void runAop() {
        loanManagerService.getTheLoanAmount();
    }

}

  • 正常日志输出:

在这里插入图片描述

可以看到正常打印了 Before、After相关信息以及自定义注解中的方法说明,方法正常执行完毕。

  • 抛出异常日志输出:

Java异常最佳实践_第1张图片

getTheLoanAmount()方法中的num1 < num2 ,将会抛出我们自定义的异常 还款本金金额大于剩余本金金额 ,从上图可以看出,除了打印Before、After以外,还打印了异常码Code、异常说明以及异常详细的堆栈信息, 大大提高了我们对于系统异常的定位,方便第一时间做出反应。

至此,这种较好的异常处理实现方式已经分享结束,这套流程的灵活度很大,这就要看具体实际的业务场景或者系统痛点,但是大差不差都是这样。


六、总结

最后,不论什么技术还是希望同学们可以灵活运行举一反三,毕竟技术都是万变不离其宗的,

就拿异常体系来讲,可以说,它可以小到你的一个测试demo,就比如本文章第五节的自定义异常实践,本身就没几行代码,我们会觉得简简单单。

但是它也可以大到市面上所有的Java相关的流行框架都在使用,有着比较复杂的抛出、捕获、处理等等,当你看到那些灵活运用异常体系的代码时,还会觉得简单吗?我想大部分人连背后的思想都没了解过吧。

所以希望同学们对技术不但要注重实践,更要思考并掌握其背后的原理,这样才能做到以不变应万变

你可能感兴趣的:(Java核心基础,java,开发语言)