【Alibaba异常处理规范】SpringBoot封装项目统一异常处理

文章介绍了Java中的异常机制,包括Error和Exception的区分,以及如何通过AbstractException抽象类来构建客户端异常、服务端异常和远程服务调用异常的分类。异常码定义遵循阿里巴巴开发手册的规则,并提供了一个5位到7位扩展的建议。最后,展示了如何在SpringBoot中使用@RestControllerAdvice进行全局异常处理。
摘要由CSDN通过智能技术生成

首先,定义统一异常处理的时候,先搞清楚几件事:

  • 什么是异常?
  • 异常如何进行分类?
  • 异常码如何定义?
  • 如何统一处理异常?

什么是异常

异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。

在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。 Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。异常和错误的区别是:异常能被程序本身可以处理,错误是无法处理。

Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。

Exception(异常):是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。

异常如何进行分类

通过上面所述,Error 属于程序无法处理的错误,我们的目标更多是针对 Exception 做些文章。而 RuntimeException 是 Exception 中重要的一个子类,我们对于异常做的种种处理都是基于 RuntimeException 而来。
在这里插入图片描述

代码位置:org.opengoofy.congomall.springboot.starter.convention.exception 目录下。

1. 抽象异常

三类异常共同继承 RuntimeException,那么肯定具备很多相似的地方,这里统一抽象一层 AbstractException。

package org.opengoofy.congomall.springboot.starter.convention.exception;

import com.google.common.base.Strings;
import lombok.Getter;
import org.opengoofy.congomall.springboot.starter.convention.errorcode.IErrorCode;

import java.util.Optional;

/**
 * 抽象项目中三类异常体系,客户端异常、服务端异常以及远程服务调用异常
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 * @see ClientException
 * @see ServiceException
 * @see RemoteException
 */
@Getter
public abstract class AbstractException extends RuntimeException {

    public final String errorCode;

    public final String errorMessage;

    public AbstractException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable);
        this.errorCode = errorCode.code();
        this.errorMessage = Optional.ofNullable(Strings.emptyToNull(message)).orElse(errorCode.message());
    }
}

2. 客户端异常

客户端相关异常,比如:参数校验失败异常、幂等异常…

package org.opengoofy.congomall.springboot.starter.convention.exception;

import org.opengoofy.congomall.springboot.starter.convention.errorcode.BaseErrorCode;
import org.opengoofy.congomall.springboot.starter.convention.errorcode.IErrorCode;

/**
 * 客户端异常
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 */
public class ClientException extends AbstractException {

    public ClientException(IErrorCode errorCode) {
        this(null, null, errorCode);
    }

    public ClientException(String message) {
        this(message, null, BaseErrorCode.CLIENT_ERROR);
    }

    public ClientException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }

    @Override
    public String toString() {
        return "ClientException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

3. 服务端异常

服务端相关异常,比如:消息模版不合规、消息内容包含关键字…

package org.opengoofy.congomall.springboot.starter.convention.exception;

import org.opengoofy.congomall.springboot.starter.convention.errorcode.BaseErrorCode;
import org.opengoofy.congomall.springboot.starter.convention.errorcode.IErrorCode;

/**
 * 服务端异常
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 */
public class ServiceException extends AbstractException {

    public ServiceException(String message) {
        this(message, null, BaseErrorCode.SERVICE_ERROR);
    }

    public ServiceException(IErrorCode errorCode) {
        this(null, errorCode);
    }

    public ServiceException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }

    @Override
    public String toString() {
        return "ServiceException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

4. 远程调用异常

服务调用远端异常,比如:短信调用三方服务发送失败、调用 MQ 失败、调用 Redis 失败等…

package org.opengoofy.congomall.springboot.starter.convention.exception;

import org.opengoofy.congomall.springboot.starter.convention.errorcode.IErrorCode;

/**
 * 远程服务调用异常
 *
 * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 */
public class RemoteException extends AbstractException {

    public RemoteException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }

    public RemoteException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }

    @Override
    public String toString() {
        return "RemoteException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

异常码如何定义

在上一篇文章中,留下一个知识点,异常码如何定义?业界并没有统一的标准,翻看腾讯、谷歌、阿里、亚马逊以及 FaceBook 等网站,也没有相似之处。这里我们按照 Java开发手册(嵩山版) 中给出的异常码规定方式,确定一版以观后效。我们先看看阿里巴巴开发手册如何规定异常码。
错误码为字符串类型,共 5 位。分成两个部分:错误产生来源 + 四位数字编号
错误产生来源分为 A / B / C

  • A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题。
  • B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题。
  • C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题。
  • 四位数字编号从 0001 到 9999,大类之间的步长间距预留 100。

1. 如何定义异常码?

编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上(需要开发)进行,审批生效,编号即被永久固定。

错误码使用者避免随意定义新的错误码。尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。

2. 异常码示例

错误码中文描述说明
00000一切 ok正确执行后的返回
A0001用户端错误一级宏观错误码
A0100用户注册错误二级宏观错误码
A0101用户未同意隐私协议
A0102注册国家或地区受限
A0110用户名校验失败
A0111用户名已存在
A0112用户名包含敏感词
A0113用户名包含特殊字符
A0120密码校验失败
A0121密码长度不够
A0122密码强度不够
A0130校验码输入错误
A0131短信校验码输入错误
A0132邮件校验码输入错误
A0133语音校验码输入错误
A0140用户证件异常
A0141用户证件类型未选择
A0142大陆身份证编号校验非法
A0143护照编号校验非法
A0144军官证编号校验非法
A0150用户基本信息校验失败
A0151手机格式校验失败
A0152地址格式校验失败
A0153邮箱格式校验失败
A0200用户登录异常二级宏观错误码
A0201用户账户不存在
A0202用户账户被冻结
A0203用户账户已作废
A0210用户密码错误
A0211用户输入密码错误次数超限
A0220用户身份校验失败
A0221用户指纹识别失败
A0222用户面容识别失败
A0223用户未获得第三方登录授权
A0230用户登录已过期
A0240用户验证码错误
A0241用户验证码尝试次数超限
A0300访问权限异常二级宏观错误码
A0301访问未授权
A0302正在授权中
A0303用户授权申请被拒绝
A0310因访问对象隐私设置被拦截
A0311授权已过期
A0312无权限使用 API
A0320用户访问被拦截
A0321黑名单用户
A0322账号被冻结
A0323非法 IP地址
A0324网关访问受限
A0325地域黑名单
A0330服务已欠费
A0340用户签名异常
A0341RSA 签名错误
A0400用户请求参数错误二级宏观错误码
A0401包含非法恶意跳转链接
A0402无效的用户输入
A0410请求必填参数为空
A0411用户订单号为空
A0412订购数量为空
A0413缺少时间戳参数
A0414非法的时间戳参数
A0420请求参数值超出允许的范围
A0421参数格式不匹配
A0422地址不在服务范围
A0423时间不在服务范围
A0424金额超出限制
A0425数量超出限制
A0426请求批量处理总个数超出限制
A0427请求JSON解析失败
A0430用户输入内容非法
A0431包含违禁敏感词
A0432图片包含违禁信息
A0433文件侵犯版权
A0440用户操作异常
A0441用户支付超时
A0442确认订单超时
A0443订单已关闭
A0500用户请求服务异常二级宏观错误码
A0501请求次数超出限制
A0502请求并发数超出限制
A0503用户操作请等待
A0504WeBSoCket连接异常
A0505WeBSoCket连接断开
A0506用户重复请求
A0600用户资源异常二级宏观错误码
A0601账户余额不足
A0602用户磁盘空间不足
A0603用户内存空间不足
A0604用户OSS容量不足
A0605用户配额已用光蚂蚁森林浇水数或每天抽奖数
A0700用户上传文件异常二级宏观错误码
A0701用户上传文件类型不匹配
A0702用户上传文件太大
A0703用户上传图片太大
A0704用户上传视频太大
A0705用户上传压缩文件太大
A0800用户当前版本异常二级宏观错误码
A0801用户安装版本与系统不匹配
A0802用户安装版本过低
A0803用户安装版本过高
A0804用户安装版本已过期
A0805用户API请求版本不匹配
A0806用户API请求版本过高
A0807用户API请求版本过低
A0900用户隐私未授权二级宏观错误码
A0901用户隐私未签署
A0902用户摄像头未授权
A0903用户相机未授权
A0904用户图片库未授权
A0905用户文件未授权
A0906用户位置信息未授权
A0907用户通讯录未授权
A1000用户设备异常二级宏观错误码
A1001用户相机异常
A1002用户麦克风异常
A1003用户听筒异常
A1004用户扬声器异常
A1005用户GPS定位异常
B0001系统执行出错一级宏观错误码
B0100系统执行超时二级宏观错误码
B0101系统订单处理超时
B0200系统容灾功能被触发二级宏观错误码
B0210系统限流
B0220系统功能降级
B0300系统资源异常二级宏观错误码
B0310系统资源耗尽
B0311系统磁盘空间耗尽
B0312系统内存耗尽
B0313文件句柄耗尽
B0314系统连接池耗尽
B0315系统线程池耗尽
B0320系统资源访问异常
B0321系统读取磁盘文件失败
C0001调用第三方服务出错
C0100中间件服务出错一级宏观错误码
C0110RPC服务出错
C0111RPC服务未找到
C0112RPC服务未注册
C0113接口不存在
C0120消息服务出错
C0121消息投递出错
C0122消息消费出错
C0123消息订阅出错
C0124消息分组未查到
C0130缓存服务出错
C0131key长度超过限制
C0132value长度超过限制
C0133存储容量已满
C0134不支持的数据格式
C0140配置服务出错
C0150网络资源服务出错
C0151VPN服务出错
C0152CDN服务出错
C0153域名解析服务出错
C0154网关服务出错
C0200第三方系统执行超时二级宏观错误码
C0210RPC执行超时
C0220消息投递超时
C0230缓存服务超时
C0240配置服务超时
C0250数据库服务超时
C0300数据库服务出错二级宏观错误码
C0311表不存在
C0312列不存在
C0321多表关联中存在多个相同名称的列
C0331数据库死锁
C0341主键冲突
C0400第三方容灾系统被触发二级宏观错误码
C0401第三方系统限流
C0402第三方功能降级
C0500通知服务出错二级宏观错误码
C0501短信提醒服务失败
C0502语音提醒服务失败
C0503邮件提醒服务失败

3. 异常码小结

阿里巴巴开发手册中的核心思想是规定常用异常码,能复用就复用,实在不行就通过异常码平台去创建,先到先得。
这里我是认可的,不过有一点感觉不是很合适,就是异常码的位数。

阿里巴巴开发手册规定异常码位数 5 位,这对于一个公司项目很多的情况下,我觉得并不适用。所以,在刚果商城中,保留异常码基础概念,但是将 5 位扩展到 7 位。
这样可以有效防止项目过多导致的异常码冲突问题。

如何统一处理异常

为了预防由于项目代码问题导致的异常情况出现,统一格式化后端异常错误响应数据。SpringBoot 提供全局拦截异常注解 @RestControllerAdvice
代码位置:

org.opengoofy.congomall.springboot.starter.web.GlobalExceptionHandlerpackage org.opengoofy.congomall.springboot.starter.web;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import org.opengoofy.congomall.springboot.starter.convention.exception.AbstractException;
import org.opengoofy.congomall.springboot.starter.convention.errorcode.BaseErrorCode;
import org.opengoofy.congomall.springboot.starter.convention.result.Result;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.Optional;

/**
 * 全局异常处理器
 *  * @author chen.ma
 * @github <a href="https://github.com/opengoofy" />
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 拦截参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
    }

    /**
     * 拦截应用内抛出的异常
     */
    @ExceptionHandler(value = {AbstractException.class})
    public Result abstractException(HttpServletRequest request, AbstractException ex) {
        if (ex.getCause() != null) {
            log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
            return Results.failure(ex);
        }
        log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
        return Results.failure(ex);
    }

    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return Results.failure();
    }

    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

在全局异常拦截中,并没有拦截过多异常,仅对三类异常做出了拦截:

  • MethodArgumentNotValidException:参数异常,这里需要提供合理的报错信息,所以单独拦截。正常来说,可根据项目需求,可拦截可不拦截。
  • AbstractException:上面有提到,基础组件中封装了三类异常,它们共有一个父类,这里相当于拦截了三种异常。
  • Throwable:拦截所有异常,属于兜底方案,如果上面异常都没有拦截到,那么这里通过异常父类来做最后处理。通过这些配置,可以很好避免下述烦人的 500 页面,哈哈。
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值