03 | 异常处理实践 - 抛异常+错误码

3 篇文章 1 订阅

系列文章目录

上一讲,我们讲了 DDD 领域驱动设计在 SpringBoot 中的实践,接下来,说一下我是如何处理异常的。

请看今天的第3讲:异常处理和错误码管理。


前言

在业务逻辑中,处理异常有两种方式:

  • 抛出异常:可以使得代码更清晰,可读性更好,更符合面向对象。
  • 返回错误码:优点是性能更好,但是不宜维护。

选择哪种需要根据场景而定,不管如何选择,只要团队达成共识,统一规范就可以。

我在 YanX 项目中使用的是混合方式:后台逻辑使用抛出异常方式;对于用户或接口,使用返回错误码方式。

自定义异常

创建一个统一的业务异常基类 BaseException ,继承运行时异常 RuntimeException ,包含两个属性:code、messgae ,和一些常用的构造方法。

这里面的属性 code 的作用就是储存错误码,以便于在返回前台时将错误码返回给用户。

@Getter
public class BaseException extends RuntimeException {
    /**
     * 错误码
     */
    private Integer code;

    /**
     * 错误消息
     */
    private String message;

    public BaseException(Throwable cause) {
        this(null, cause.getMessage(), cause);
    }

    public BaseException(Integer code, String message) {
        this(code, message, null);
    }

    public BaseException(Integer code, Throwable cause) {
        this(code, cause.getMessage(), cause);
    }

    public BaseException(Integer code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
        this.message = message;
    }
}

抛出异常:

public void demo() {
    throw new BaseException(999, "系统错误");
}

错误码管理

上面的自定义异常看起来很平平无奇,但是怎么将错误码和错误信息管理起来,使代码更优雅简单,是我们接下来要解决的问题。

我使用了 Enum 枚举,先创建一个接口,供枚举来实现,其中包含两个方法:

  • toCode:将枚举值转为整形错误码;
  • getMsg:获取枚举中的异常信息。
public interface IExceptionEnum {
    /**
     * 获取异常编码
     *
     * @return
     */
    default int toCode() {
        StringBuilder num = new StringBuilder();
        for (char c : this.toString().toCharArray()) {
            if (CharUtil.isNumber(c)) {
                num.append(c);
            }
        }
        return Convert.toInt(num, -1);
    }
    /**
     * 获取异常信息
     *
     * @return
     */
    String getMsg();
}

下面创建一个枚举类,实现上面的接口:

@Getter
@AllArgsConstructor
public enum SystemError implements IExceptionEnum {
    /**
     * 定义系统异常
     */
    E990("自定义异常"),
    E996("权限异常"),
    E997("票据异常"),
    E998("请求异常"),
    E999("系统错误");

    private String msg;
}

观察上面的错误码枚举类,我们发现,枚举值为字母+错误码,属性 msg 为错误信息。

这样就把错误码异常信息统一管理了起来,抛出异常可优化为:

public void demo() {
    throw new BaseException(SystemError.E990.toCode(), SystemError.E990.getMsg());
}

然而这样依然不够优雅,代码量比之前还要长。要是直传枚举值一个参数就好了,那么我们继续优化。

创建一个异常类 BusinessException ,继承 BaseException (保证 BaseException 的简单性,创建一个子类,用来接收枚举值),如下:

public class BusinessException extends BaseException implements Serializable {
    private static final long serialVersionUID = 1L;

    public BusinessException() {
        this(SystemError.E990.getMsg());
    }

    public BusinessException(String msg, Object... objects) {
        super(SystemError.E990.toCode(), String_.format(msg, objects));
    }

    public BusinessException(Throwable cause) {
        super(cause instanceof BaseException ? ((BaseException) cause).getCode() : SystemError.E999.toCode(), cause);
    }

    public BusinessException(IExceptionEnum exceptionEnum, Object... objects) {
        this(null, exceptionEnum, objects);
    }

    public BusinessException(Throwable cause, IExceptionEnum exceptionEnum, Object... objects) {
        super(exceptionEnum.toCode(), String_.format(exceptionEnum.getMsg(), objects), cause);
    }
}

我们主要看下最后两个构造函数,使用它们,我们抛出异常的代码可优化为:

public void demo() {
    throw new BusinessException(SystemError.E990);
}

如果想要保留原异常信息,还可以使用:

public void demo() {
    try {
        //业务代码
    } catch (Exception e) {
        throw new BusinessException(e, SystemError.E990);
    }
}

统一异常处理

下面我们使用 @ControllerAdvice 和 @ExceptionHandler 注解做一下统一异常处理:

  • 业务异常打印到日志中
  • 系统异常封装为 BaseException 进行返回,同样打印日志;
  • 这里也可以做其他操作,比如短信提醒等。

代码如下:

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 系统异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public BaseException handler(Exception e) {
        BaseException be = new BusinessException(e);
        this.logException(be);
        return be;
    }

    /**
     * 打印异常信息
     *
     * @param be
     */
    private void logException(BaseException be) {
        Throwable cause = be.getCause();
        if (cause == null) {
            cause = be;
        }
        log.error("E{} : {} -> {}\n{}", be.getCode(), be.getMessage(), cause.getClass().getName(), getThrowMsg(cause));
    }

    private String getThrowMsg(Throwable cause) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < cause.getStackTrace().length; i++) {
            String str = cause.getStackTrace()[i].toString();
            //只显示前20行
            if (i < 20) {
                sb.append(str).append("\r\n");
            } else {
                break;
            }
        }
        return sb.toString();
    }
}

最后

本讲介绍了我对异常的处理方式,原理很简单,主要注重代码的可读性和优雅性,希望能够给大家提供一个思路。项目代码已托管到 Gitee(搜索 yanx ),大家可自行下载学习,如果有什么问题请在下方留言。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值