SpringBoot-23-全局异常机制+RESTful统一规范
1.为什么需要全局异常机制?
如果我们团队在开发项目的时候,程序出现异常是正常的,比如因为业务操作没有按照流程,程序的运行异常等等,我们不可能也不应该每一处异常进行单独处理,或者不处理将异常信息直接抛出给用户,这样会导致用户体验差。
因此设置统一的异常处理机制具有以下好处:
- 输出日志可以增强log的可读性,对我们自己排除bug也增加优势,
- 提高用户体验性
- 降低前后端开发和维护成本(如果每一个后台开发抛出一场形式不一样,没有统一规范,前端每一个请求都会有一套处理异常逻辑,代码质量低,维护成本高)
那么我们需要如何的开发规范呢?
Service、Controlle等层捕获的异常,需要转换为自定义异常,然后对外抛出
设置统一的RESTful规范,常见的状态使用http状态,如果是业务比较复杂的,比如智能制造类系统,也要设置自定义的状态和相对应的message。
将捕获的异常message信息,通过自定义异常转换为易读易懂的message信息。
获取catch的时候,不要直接Exception,要尽可能分类exception,使得异常变得清晰。
之前章节也讲过
全局异常处理和
RESTful统一规范响应数据格式
不熟悉的朋友可以去看一下。
代码实现
RESTful统一返回规范设置
我们在上一章节实际上已经介绍过了,但是怕一些人没有看过,这里再进行介绍一次
- 设置统一的IResultCode返回码接口
/** * 统一返回结果接口 */ public interface IResultCode { /** * 返回码 * * @return int */ int getCode(); /** * 返回消息 * * @return String */ String getMsg(); }
- 设置ResultCode的返回码接口的实现
@Getter @AllArgsConstructor public enum ResultCode implements IResultCode{ /** * 操作成功 */ SUCCESS(200, "操作成功"), /** * 业务异常 */ FAILURE(400, "业务异常"), /** * 服务异常 */ ERROR(500, "服务异常"), /** * 参数错误 */ GLOBAL_PARAM_ERROR(4000, "参数错误"); /** * 状态码 */ final int code; /** * 消息内容 */ final String msg; }
- 统一返回结果Result
@Data @Getter public class Result<T> implements Serializable { private static final long serialVersionUID = 1L; private int code; private String msg; private long time; private T data; private Result() { this.time = System.currentTimeMillis(); } private Result(IResultCode resultCode) { this(resultCode, null, resultCode.getMsg()); } private Result(IResultCode resultCode, String msg) { this(resultCode, null, msg); } private Result(IResultCode resultCode, T data) { this(resultCode, data, resultCode.getMsg()); } private Result(IResultCode resultCode, T data, String msg) { this(resultCode.getCode(), data, msg); } private Result(int code, T data, String msg) { this.code = code; this.data = data; this.msg = msg; this.time = System.currentTimeMillis(); } /** * 返回状态码 * * @param resultCode 状态码 * @param <T> 泛型标识 * @return ApiResult */ public static <T> Result<T> success(IResultCode resultCode) { return new Result<>(resultCode); } public static <T> Result<T> success(String msg) { return new Result<>(ResultCode.SUCCESS, msg); } public static <T> Result<T> success(IResultCode resultCode, String msg) { return new Result<>(resultCode, msg); } public static <T> Result<T> data(T data) { return data(data, "处理成功"); } public static <T> Result<T> data(T data, String msg) { return data(ResultCode.SUCCESS.code, data, msg); } public static <T> Result<T> data(int code, T data, String msg) { return new Result<>(code, data, data == null ? "承载数据为空" : msg); } public static <T> Result<T> fail() { return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg()); } public static <T> Result<T> fail(String msg) { return new Result<>(ResultCode.FAILURE, msg); } public static <T> Result<T> fail(int code, String msg) { return new Result<>(code, null, msg); } public static <T> Result<T> fail(IResultCode resultCode) { return new Result<>(resultCode); } public static <T> Result<T> fail(IResultCode resultCode, String msg) { return new Result<>(resultCode, msg); } public static <T> Result<T> condition(boolean flag) { return flag ? success("处理成功") : fail("处理失败"); } }
全局异常处理
- 设置BaseException作为全局基础异常和特定的验证码异常处理类
@Data public class BaseException extends RuntimeException { private static final long serialVersionUID = 5782968730281544562L; private int status = INTERNAL_SERVER_ERROR.value(); public BaseException() { } public BaseException(String message) { super(message); } public BaseException(int status, String message) { super(message); this.status = status; } } @Data public class ValidateCodeException extends RuntimeException { private static final long serialVersionUID = -7285211528095468156L; private int status = INTERNAL_SERVER_ERROR.value(); public ValidateCodeException() { } public ValidateCodeException(String msg) { super(msg); } public ValidateCodeException(int code, String message) { super(message); this.status = code; } }
- 创建全局异常处理类BaseExceptionHandler
使用**@RestControllerAdvice+@ExceptionHandler**
具体介绍可以看我的全局异常处理介绍这里只说实现过程
@Slf4j @ResponseBody @RestControllerAdvice public class BaseExceptionHandler { /** * BaseException 异常捕获处理 * @param ex 自定义BaseException异常类型 * @return Result */ @ExceptionHandler(BaseException.class) public Result<?> handleException(BaseException ex) { log.error("程序异常:" + ex.toString()); return Result.fail(ex.getStatus(), ex.getMessage()); } /** * BaseException 异常捕获处理 * @param ex 自定义BaseException异常类型 * @return Result */ @ExceptionHandler(ValidateCodeException.class) @ResponseStatus public Result<?> handleValidateCodeException(ValidateCodeException ex) { log.error("验证码错误:" + ex.getMessage()); return Result.fail(ex.getStatus(), ex.getMessage()); } /** * FileNotFoundException,NoHandlerFoundException 异常捕获处理 * @param exception 自定义FileNotFoundException异常类型 * @return Result */ @ExceptionHandler({FileNotFoundException.class, NoHandlerFoundException.class}) public Result<?> noFoundException(Exception exception) { log.error("程序异常==>errorCode:{}, exception:{}", HttpStatus.NOT_FOUND.value(), exception.getMessage()); return Result.fail(HttpStatus.NOT_FOUND.value(), exception.getMessage()); } /** * NullPointerException 空指针异常捕获处理 * @param ex 自定义NullPointerException异常类型 * @return Result */ @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<?> handleException(NullPointerException ex) { log.error("程序异常:{}" + ex.toString()); return Result.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage()); } /** * 通用Exception异常捕获 * @param ex 自定义Exception异常类型 * @return Result */ @ExceptionHandler @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<?> handleException(Exception ex) { log.error("程序异常:" + ex.toString()); String message = ex.getMessage(); if (StringUtils.contains(message, "Bad credentials")){ message = "您输入的密码不正确"; } else if (StringUtils.contains(ex.toString(), "InternalAuthenticationServiceException")) { message = "您输入的用户名不存在"; } return Result.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), message); } }
控制层TestController的实现
@RequestMapping("test") @RestController public class TestController { @GetMapping("/base/{name}") public void BaseException(@PathVariable("name") String name) { System.out.println("Hello: BaseException "+ name); throw new BaseException(HttpStatus.MULTI_STATUS,"错误"); } @GetMapping("/valid/{name}") public void ValidateCodeException(@PathVariable("name") String name) { System.out.println("Hello: ValidateCodeException "+ name); throw new ValidateCodeException(ResultCode.GLOBAL_PARAM_ERROR.getCode(),ResultCode.GLOBAL_PARAM_ERROR.getMsg()); } }
测试
使用postman分别测试
- http://localhost:8080/test/base/hah
- http://localhost:8080/test/valid/hah
测试结果如下:
在上面图标记处不知道大家发现一个问题,自定义业务状态码和HttpStatus状态码存在不一致的情况,如果相应自定义的业务状态码,在HttpStatus存在,相应HttpStatus和自定义状态码一致需要怎么办呢?
HTTP和自定义状态码一致代码实现
@Component @ControllerAdvice public class GlobalResponseAdvice implements ResponseBodyAdvice { @Override public boolean supports(MethodParameter returnType, Class converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { //如果响应结果是JSON数据类型 if(selectedContentType.equalsTypeAndSubtype( MediaType.APPLICATION_JSON)){ int code= ((Result) body).getCode(); if(code>0 && code<512) { //HTTP响应结果设置状态码,状态码就是IResultCode的code,二者达到统一 response.setStatusCode( HttpStatus.valueOf(((Result) body).getCode()) ); } return body; } return body; } }
测试结果: