如何优雅的处理全局异常

前言

异常处理是项目开发中绕不过的一个环节,一个优雅的全局异常处理可以迅速反馈给开发人员这些信息

1、异常种类

2、可能导致异常的原因

3、导致异常出现的关键参数

4、异常发生的时间

5、发生异常的请求路径

这些信息有助于开发人员迅速定位、处理异常,一个优秀的项目应该尽可能的将可能发生的异常进行捕获,再通过自定义的处理流程将异常信息反馈,而不是一味的抛出异常

异常捕获流程

ErrorCode 异常信息枚举

​ 对于异常的处理我倾向于通过枚举类列举广义的异常种类,再通过附加信息对异常种类进行细分。

​ 枚举类可以规范管理异常的种类,方面异常对象的构建,但是不利于异常种类的细化,比如A业务类抛出了C资源不存在的错误,B业务类同样抛出了D资源不存在的错误,那么从广义上来说这两种异常都属于RESOURCE_NOT_FOUNT异常,但是我们无法区分这个异常是因为C资源不存在还是D资源不存在而出现的异常。大多数开源项目更倾向于通过构建异常的Message来对异常类型进行细分,而通过创建ResourceException这样具体的异常类来对异常种类进行粗略的区分。所以我们可以通过构建异常Message枚举类相结合的方式来弥补这一点。

@Getter
@AllArgsConstructor
public enum ErrorCode  {

    RESOURCE_NOT_FOUNT(1001, HttpStatus.HTTP_NOT_FOUND,"未找到该资源"),
    PARAMS_FORMAT_INVALID(1002,HttpStatus.HTTP_BAD_REQUEST,"请求参数格式错误");

    private final int code;

    private final int httpStatus;

    private final String message;

}

BaseException 基础异常对象

BaseException 是所有自定义异常的父类,它规范了异常的基本结构由 ErrorCode 和 Map(附加信息)组成

@Getter
@Setter
public class BaseException extends RuntimeException {

    private final ErrorCode code;

    private final Map<String,Object> detail = new HashMap<>();

    public BaseException(ErrorCode code) {
        this.code = code;
    }

    public BaseException(ErrorCode code,Map<String,Object> params) {
        this.code = code;
        if (!ObjectUtils.isEmpty(params)) {
            this.detail.putAll(params);
        }
    }
	
    public BaseException(ErrorCode code,String msg) {
        this.code = code;
        HashMap<String,String> map = new HashMap<>();
        map.put("description",msg);
        detail.putAll(map);
    }


ErrorVO 异常视图对象

定义一个异常视图对象,构建异常的基本信息,比如时间,请求路径等,该对象接收一个 BaseException 异常类

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ErrorVO {
    //异常编码
    private Integer code;
    //http状态码
    private Integer httpStatus;
    //信息
    private String message;
    //发生异常的请求
    private String path;
    //异常发生时间戳
    private Instant timestamp;
    //异常附加信息
    private Map<String, Object> detail = new HashMap<>();

    public ErrorVO(BaseException e,String path) {
        this.code = e.getCode().getCode();
        this.httpStatus = e.getCode().getHttpStatus();
        this.timestamp = Instant.now();
        this.path = path;
        this.message = e.getCode().getMessage();
        if (ObjectUtils.isEmpty(e.getDetail())){
            this.detail = null;
        }else {
            this.detail.putAll(e.getDetail());
        }
    }
}

全局返回对象 Result

我认为一个项目的请求返回对象应该做到全局统一,这样开发的时候就不用纠结到底该返回哪个对象

@Getter
@Setter
@AllArgsConstructor
public class Result<T> {

    //操作成功(true)或者失败(false)
    private boolean success;

    //封装的数据对象,传入vo或者自定义的Exception类
    private T data;

    public static Result<Object> ok() {
        return new Result<>(true,null);
    }

    public static Result<String> ok(String msg) {
        return new Result<>(true,msg);
    }
    
    public static Result<Object> ok(Object data) {
        return new Result<>(true,data);
    }

    public static Result<Object> error() {
        return new Result<>(false,null);
    }

    public static Result<String> error(String msg) {
        return new Result<>(false,msg);
    }
    
    public static Result<ErrorVO> error(ErrorVO error) {
        return new Result<>(false,error);
    }

}

异常捕获类

@ControllerAdvice表明被注释的类是一个通知增强类,如果希望返回对象格式为 json 可以使用注解@RestControllerAdvice,可以使用注解@ExceptionHandler来匹配所有被@RequestMapping注释的方法中抛出的异常,如果匹配成功,那么就按照自定义的流程去处理该异常

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BaseException.class)
    public Result<ErrorVO> baseException(BaseException e, HttpServletRequest request) {
        ErrorVO errorVO = new ErrorVO(e,request.getRequestURI());
        return Result.error(errorVO);
    }
}

异常捕获效果

测试类

测试采用三种方式来构建异常对象,一种是无附加信息,第二种是通过拼接 Message 返回附加信息,第三种是通过构建 Map 对象返回异常附加信息

@RestController
@RequestMapping("/test")
public class TestController {
    
    @GetMapping("/resource")
    public Result<Object> resource(){
        throw new BaseException(ErrorCode.RESOURCE_NOT_FOUNT);
    }

    @GetMapping("/id")
    public Result<Object> test(@Param("id")String id) {
        if (id.length()>5){
            throw new BaseException(ErrorCode.RESOURCE_NOT_FOUNT,"The invalid id is:\n" + id);
        }else {
            return Result.ok();
        }
    }

    @PostMapping("/resource")
    public Result<Object> resource(@RequestBody Car car){
        throw new BaseException(ErrorCode.PARAMS_FORMAT_INVALID,
                ImmutableMap.of("category",car.getCategory(),
                                "name",car.getName(),
                                "number",car.getNumber()));
    }
}

效果展示

这三种返回信息基本满足了日常开发中对于异常信息的描述,当然对于异常的捕获处理一定还有更好的方式,这种方式只是我个人觉得比较方便,并不意味着这就是最好的方案了,欢迎大家补充

第一种方式
{
    "success": false,
    "data": {
        "code": 1001,
        "httpStatus": 404,
        "message": "未找到该资源",
        "path": "/test/resource",
        "timestamp": "2020-06-29T19:25:40.138Z",
        "detail": null
    }
}

第二种方式
{
    "success": false,
    "data": {
        "code": 1002,
        "httpStatus": 400,
        "message": "请求参数格式错误",
        "path": "/test/id",
        "timestamp": "2020-06-29T19:25:15.297Z",
        "detail": {
            "description": "The invalid id is: 1111111111"
        }
    }
}

第三种方式
{
    "success": false,
    "data": {
        "code": 1001,
        "httpStatus": 404,
        "message": "未找到该资源",
        "path": "/test/resource",
        "timestamp": "2020-06-29T19:29:41.032Z",
        "detail": {
            "name": "ChangAn-1",
            "number": "普A-123456",
            "category": "smallCar"
        }
    }
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值