【异常设计】自定义异常体系,异常处理中心

在Java程序开发中,会遇到各种异常,常见的NullPonitException(NPE),IllegalArgumentException等等,Java的优雅之处,很大一部分就在于这些刚开始看着头痛的异常中

Exceptions

首先,什么是异常,通俗来讲,异常,是我们程序出现错误,并无法按照预期执行下去的情况,会抛出一个异常,它会告诉你,某个地方出错了,请示你该如何解决或者修改你的代码。

那么我们可以简单理解为,这是一个消息,一旦程序遇到问题,原定逻辑无法执行,程序非常忠实于开发者,它不会自己决定该怎么做,此时,程序开始收集信息,例如程序在A类执行到118行代码的地方,遇到了一个无法解决的问题,原定逻辑设置了循环了10[0-9]次,但给定的数组里只有8条数据,该怎么解决?

当执行到index=8时,给定数组中不存在第8个索引,这里我们遇到的异常是ArrayIndexOutOfBoundsException,给你传递了一个消息:Index: 8, Size: 8,告诉你说,你给了我一个只有8个数据的List,却要我找位置在第8索引,数组越界了,同时,他给你带上了异常栈,最上面是这个异常最开始发生的代码所在位置,然后一级一级往外传,每跳出一层,就记录一级的信息,在没有异常处理的情况下,直接跳到main方法中,展示到控制台中,并终止自己。

了解了异常是什么,下面来了解下Java 自身的异常体系

Throwable

Throwable就是Java异常体系的起点,基于所有Java的类都显式或隐式的继承于Object类,所以说异常体系的Root对象,就是Throwable,Throwabl旗下有两员悍将,Error,Exception。

其中Error是主战将军,轻易不出手,出手即打残,一旦我们看到Error级别的错误,表示JVM检测到无法预期的错误,因为是JVM导致的错误,并非程序可以改变,所以最多我们可以看到异常信息,无法捕获处理。根据错误去找环境哪里配置的不正确,或者重装一次Java,通常会解决问题

另外一个Exception类,是文官,文官通常讲究游说之道,哪里不合适我们再谈谈,但这种谈也分强硬方CheckedException和弱势方RunntimeException,第一种是直接继承自Exception,这种强硬的方式,必须要给出处理办法,如果你调用的方法中可能抛出这种错,那么你必须在方法调用的地方进行处理,或者选择继续往上抛,否则编译不通过,常见的CheckedExceptionIOException。另外一种方式,就很柔和了,但是阴损,这种方式就是继承自RunntimeException的异常,处理不处理都行,不往上层抛也行,反正也不告诉你这块能出异常,你要遇到了就加上异常处理,你要遇不到,就等客户遇到了告诉你。

下面是一张Throwable的Diagrams图,看不清的朋友右键图片下载到本地放大来看
在这里插入图片描述

自定义异常体系

是不是可以基于Java的异常体系来搭建属于我们自己的呢?

首先我来描述一个场景,一个电商购物车下单业务,主业务逻辑为

  1. 校验库存,减库存
  2. 校验商品可售性
  3. 校验购物车预设的活动满减策略、组合策略、是否跨店铺
  4. 购物车拆单、子单分别发往对应商家
  5. 商家发货。。。

这个场景相对复杂,请同学们想象一下,这个下单场景调用很深,各种服务和类嵌套,其中校验商品可售性的逻辑有大概10个类,如果单纯用return来控制逻辑,会有多乱,每层返回都要做一次校验,一旦忘记拦截,就会造成意想不到的结果。

此时就需要异常来控制了,通常这种复杂场景都会有主流程服务来控制,按照1-5步骤,顺序执行服务,每个步骤就是一个场景行为(action), 那么如果用异常来处理,我只要要在对应的场景行为调用上套一层自己的业务异常处理类就可以了对吧

此时我需要区分,什么错是我们业务异常,什么是数据库异常,如果我们没有自定义异常体系,只能依赖拦截RuntimeException来做是不可能实现上述需求的,那么解决这个首先,我们先创建一个类,假设ElBasicException extend RuntimeException,并实现所需要的构造函数,通常我们不会直接在业务中抛出BasicException,假设我们在控制层做参数校验,检测出某一个参数长度超长,这时候我们可以新建一个异常,命名为ElParameterByteArrayOutOfMaxException,并继承于ElBasicException,抛出这异常,并在全局异常处理器中拦截,判断如果 e instanceof ElBasicException,那么就走我们业务的处理机制,如果不是,那么返回“系统错误“。

来看下实现:

/**
 * 扩展运行时异常
 * since 2019/12/7
 *
 * @author eddie
 */
@Slf4j
public class ExtendRuntimeException extends RuntimeException{

    private ErrorMessage errorMessage;


    public ExtendRuntimeException() {
        this(ErrorMessage.EMPTY_ERROR_MESSAGE);
        this.errorMessage = ErrorMessage.EMPTY_ERROR_MESSAGE;
        log.error("无法定义的异常, {} : {}", getErrorCode(), getErrorMessage());
    }

    public ExtendRuntimeException(ErrorMessage errorMessage) {
        super(errorMessage.getErrorMessage());
        this.errorMessage = errorMessage;
        log.error("{} : {}", getErrorCode(), getErrorMessage());
    }

    public ExtendRuntimeException(ErrorMessage errorMessage, Throwable cause) {
        super(errorMessage.getErrorMessage(), cause);
        this.errorMessage = errorMessage;
        log.error("{} : {}", getErrorCode(), getErrorMessage(), cause);
    }

    public String getErrorCode(){
        if (Objects.isNull(errorMessage)){
            return "";
        }
        return errorMessage.getErrorCode();
    }

    public String getErrorMessage(){
        if (Objects.isNull(errorMessage)){
            return "";
        }
        return errorMessage.getErrorMessage();
    }
}

扩展基类不用太复杂,但这里可以加一些监控配置,比如这里打日志,通过重写logbackappender,把这里的异常写入ELK监控里,不过appender要搞成异步缓存的,缓存一个队列,打印和网络传输都走移步,曾经有人测试异步打印和同步打印的性能区别,结果我忘记了,但差距不小。这里如果还加了网络IO,还有握手挥手的时间,更应该异步处理。

这里的ErrorMessage其实是对错误信息的封装,其中包含错误码、错误描述等自定义信息,想放啥放啥,还可以做一些时区,日志格式、多语言等封装
我们的所有异常都要基于这个异常来扩展,比如如果是DDD领域模型开发的话,每个域都应该有一个自己的基类异常,那领域服务中的异常都应该继承于这个异常,最终我们在下单服务中捕获异常,就直接捕获ExtendRuntimeException就好了

那还有个问题,如果每个域服务中的异常都不一样,那下单服务中该如何处理呢?这里就涉及到了文章开头提及的集权异常控制中心

异常控制中心

首先定义一个异常处理声明接口,这个接口有两个方法,一个是返回该领域标识符,另外一个是处理异常逻辑

/**
 * 异常处理器负责的领域
 * since 2019/12/22
 *
 * @author eddie
 */
public interface ExceptionHandler {

    /**
     * 返回异常处理器所在领域
     * @return  异常处理器所在领域
     */
    String ofExceptionDomain();

    /**
     * 处理运行时异常
     * @param e     异常
     * @return      错误信息
     */
    ErrorMessage handleException(ExtendRuntimeException e);

    /**
     * 处理 <p>checked</p> 异常
     * @param e     异常
     * @return      错误信息
     */
    ErrorMessage handleException(ImportantErrorException e);

}

最近在公司内接触到了另一个想法,只使用一个异常,例如我们这里的ExtendRuntimeException,然后里面会有丰富的接口,包括扩展执行等等,最终通过错误码来处理不同的逻辑,这样固然简单,但局限了一些东西,不是特别灵活

  1. 如果同一种类的异常有多个错误码,例如
    • 金额异常(103) 错误码 10301 金额格式不正确
    • 金额异常(103) 错误码 10302 货币种类不支持
    • 金额异常(103) 错误码 10303 汇率波动,请稍后重试
    • 。。。。
  2. 只想捕获自己的异常,如果所有的异常都是ExtendRuntimeException,那别人写的异常也会被我捕获

所以互相比较来看,如果业务复杂,团队较大,需要管理的领域团队不同,就是用相对灵活一点的方案。
至于handleException方法,为什么还返回一个ErrorMessage。其实是为了翻译异常。有些内部异常的错误码只希望打印在控制台上,不透出给外部,另外异常控制中心不应该处理业务逻辑,也就是说异常一旦抵达这里,只能返回给前台了。

通常按照规范,一个合格的系统,所有的异常都应该是有预期的,也就是不会抛出我们不可预见的错误,预期中的错误,都应该有通用或特殊处理的逻辑来捕获处理,预期外的错误,遇到就需要立即解决,或在异常抛出位置进行转译,但不推荐这么做。

另外,最好只在必须要抛异常来触发某些操作时抛异常,否则尽量减少抛异常的次数和抛异常的可能,因为我们知道Java的方法调用其实是压栈,方法调用结束就会弹栈,如果在方法调用中出现异常,它会记录当前栈的信息,回到上一层,上一层没有捕获继续往上走,每一次弹出栈都会记录一次信息,属于昂贵消耗,尤其我们在Spring项目中,Spring代码调用层次很深,并且在Spring内部不会为你处理异常信息,所以通常spring项目报错,我们会看到很长很长的异常栈信息,新手甚至找不到这个错误信息是由哪个地方引起的。

ps:理论上我们不应该使用异常来控制业务逻辑,另外异常处理会挂载堆栈信息,调用栈越深,效率越低。但有时系统过于复杂时,为了减少系统复杂度,用异常来中断后续操作也不失为一种优雅的方案

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在Java中,异常是一种特殊的对象,表示在程序运行期间发生的不正常情况。异常体系是由Java语言提供的一组类和接口,用于描述异常和处理异常Java中的异常体系由两个主要部分组成: 1. 异常类:Java中的异常类都是 `Throwable` 类的子类,其中包括两种常见的子类:`Error` 和 `Exception`。 - `Error` 类表示严重的错误,通常是指与虚拟机相关的问题,例如内存溢出或栈溢出。通常情况下,我们不会处理 `Error` 类型的异常。 - `Exception` 类表示一般性异常,它可以被捕获并处理。 `Exception` 类有两个常见的子类:`RuntimeException` 和 `IOException`。`RuntimeException` 类表示运行时异常,通常是由程序逻辑错误引起的;`IOException` 类表示输入/输出异常,通常是由文件读写或网络通信引起的。 2. 异常处理机制:Java提供了两种方法来处理异常: - `try-catch` 语句:使用 `try-catch` 语句捕获异常的过程称为异常捕获。在 `try` 块中的代码可能会抛出异常,因此我们可以在 `try` 块后面跟着一个或多个 `catch` 块来处 ### 回答2: Java中的异常体系是指用来处理程序中出现的错误和异常情况的一套机制。异常是在程序执行过程中可能发生的非正常情况,如除零错误、空指针引用等。Java异常处理机制分为3种类型:可检查异常、运行时异常和错误。 可检查异常(checked exception)是指在程序编译时就必须处理的异常。如果一个方法可能会抛出可检查异常,要么在方法的声明中使用throws关键字声明该异常,要么使用try-catch块捕获并处理异常。可检查异常的目的是提醒开发者对可能的异常情况进行处理,确保程序的健壮性。 运行时异常(runtime exception)是指不需要在编译阶段处理的异常。这些异常通常由程序错误导致,如数组越界、空指针引用等。运行时异常不需要强制处理,但可以选择使用try-catch块处理。同时,运行时异常也可以使用throws关键字声明,将异常抛给调用者。 错误(error)是指严重的错误,通常是虚拟机无法恢复的问题。错误无法通过捕获和处理来解决,常见的错误类型有OutOfMemoryError和StackOverflowError等。一旦错误发生,程序的执行将中断。 Java异常体系中,所有的异常都继承自Throwable类。Throwable类有两个最重要的子类:Exception和Error。Exception类表示可检查异常,而Error类表示错误。此外,还有RuntimeException类,它是Exception的子类,用于表示运行时异常。 通过使用Java异常体系,开发者能够在程序中处理和恢复各种异常情况,提高程序的可靠性和稳定性。在编写Java程序时,合理地处理异常是十分重要的一项技能。 ### 回答3: Java异常体系Java编程语言中用于处理程序运行错误的一种机制。在Java中,异常分为可检查异常和不可检查异常两种。 可检查异常(Checked Exception)是指需要在代码中显式处理的异常,这些异常一般表示程序处理外部资源时可能出现的错误情况,如文件读写错误、网络连接错误等。可检查异常需要使用try-catch语句来捕获并处理,或者使用throws关键字声明在方法的声明中,让调用者处理。可检查异常的父类是Exception。 不可检查异常(Unchecked Exception)也称为运行时异常(Runtime Exception),是指在代码中不需要显式处理的异常。不可检查异常一般表示程序出现了严重的错误,如除零错误、空指针异常等。不可检查异常可以选择性地使用try-catch语句捕获并处理,但不是强制要求。不可检查异常的父类是RuntimeException。 Java异常体系是通过抛出和捕获异常来实现的。当程序出现异常时,会抛出一个异常对象,该对象会沿着方法调用栈被传递到调用栈的上层,直到被捕获并进行处理。异常对象包含异常的类型和相关的信息,如异常的原因和调用栈等。捕获异常通常使用try-catch语句块来实现,try块中包含可能抛出异常的代码,catch块则用于捕获并处理异常Java异常体系还支持自定义异常,可以根据程序的需要创建自己的异常类。自定义异常类一般继承自Exception或RuntimeException。通过自定义异常,可以更好地表示程序中的特定错误,并提供更加详细的错误信息。 总之,Java异常体系是一种用于处理程序运行错误的机制,通过抛出和捕获异常来实现。通过可检查异常和不可检查异常,可以针对不同的错误情况进行适当的处理。同时,Java还支持自定义异常,可以提供更加详细和准确的错误信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你向着阳光奔跑的背影

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值