前言
异常处理是项目开发中绕不过的一个环节,一个优雅的全局异常处理可以迅速反馈给开发人员这些信息
1、异常种类
2、可能导致异常的原因
3、导致异常出现的关键参数
4、异常发生的时间
5、发生异常的请求路径
这些信息有助于开发人员迅速定位、处理异常,一个优秀的项目应该尽可能的将可能发生的异常进行
捕获 ,再通过自定义的处理流程将异常信息反馈,而不是一味的抛出 异常
异常捕获流程
ErrorCode 异常信息枚举
? 对于异常的处理我倾向于通过枚举类列举广义的异常种类,再通过附加信息对异常种类进行细分。
? 枚举类可以规范管理异常的种类,方面异常对象的构建,但是不利于异常种类的细化,比如
A 业务类抛出了C 资源不存在的错误,B 业务类同样抛出了D 资源不存在的错误,那么从广义上来说这两种异常都属于RESOURCE_NOT_FOUNT 异常,但是我们无法区分这个异常是因为C 资源不存在还是D 资源不存在而出现的异常。大多数开源项目更倾向于通过构建异常的Message 来对异常类型进行细分,而通过创建ResourceException 这样具体的异常类来对异常种类进行粗略的区分。所以我们可以通过构建异常Message 和枚举类 相结合的方式来弥补这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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(附加信息)组成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @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 异常类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @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
我认为一个项目的请求返回对象应该做到全局统一,这样开发的时候就不用纠结到底该返回哪个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @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 注释的方法中抛出的异常,如果匹配成功,那么就按照自定义的流程去处理该异常
1 2 3 4 5 6 7 8 9 | @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 对象返回异常附加信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | @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())); } } |
效果展示
这三种返回信息基本满足了日常开发中对于异常信息的描述,当然对于异常的捕获处理一定还有更好的方式,这种方式只是我个人觉得比较方便,并不意味着这就是最好的方案了,欢迎大家补充
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | 第一种方式 { "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" } } } |