如何优雅的处理全局异常

    技术2022-07-10  106

    前言

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

    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" } } }
    Processed: 0.016, SQL: 9