全局异常处理及参数校验-SpringBoot 2.7.2 实战基础 (建议收藏) ...

打印 上一主题 下一主题

主题 868|帖子 868|积分 2604

优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验

前后端分离开发非常普遍,后端处理业务,为前端提供接口。服务中总会出现很多运行时异常和业务异常,本文主要讲解在 SpringBoot 实战中如何进行异常统一处理和请求参数的校验。
1 异常统一处理

所有异常处理相关的类,咱们都放到 com.yygnb.demo.common包中。
当后端发生异常时,需要按照一个约定的规则(结构)返回给前端,所以先定义一个发生异常时固定的结构。
1.1 错误响应结构

发生异常时的响应结构约定两个字段:code——错误编码;msg——错误消息。创建类:
com.yygnb.demo.common.domain.ErrorResult:
  1. @Data
  2. @Builder
  3. @NoArgsConstructor
  4. @AllArgsConstructor
  5. public class ErrorResult implements Serializable {
  6.     private static final long serialVersionUID = -8738363760223125457L;
  7.     /**
  8.      * 错误码
  9.      */
  10.     private String code;
  11.     /**
  12.      * 错误消息
  13.      */
  14.     private String msg;
  15.   
  16.     public static ErrorResult build(ErrorResult commonErrorResult, String msg) {
  17.         return new ErrorResult(commonErrorResult.getCode(), commonErrorResult.getMsg() + " " + msg);
  18.     }
  19. }
复制代码
1.2 通用错误响应常量

有些异常返回的 ErrorResult 是一样的,如参数校验错误、未查询到对象、系统异常等,可以定义一些错误响应的常量:
com.yygnb.demo.common.exception.DefaultErrorResult:
  1. public interface DefaultErrorResult {
  2.     ErrorResult SYSTEM_ERROR = new ErrorResult("C00001", "系统异常");
  3.     ErrorResult CUSTOM_ERROR = new ErrorResult("C99999", "自定义异常");
  4.     ErrorResult PARAM_BIND_ERROR = new ErrorResult("C00003", "参数绑定错误:");
  5.     ErrorResult PARAM_VALID_ERROR = new ErrorResult("S00004", "参数校验错误:");
  6.     ErrorResult JSON_PARSE_ERROR = new ErrorResult("S00005", "JSON转换异常");
  7.     ErrorResult CODE_NOT_FOUND = new ErrorResult("S00006", "根据编码没有查询到对象");
  8.     ErrorResult ID_NOT_FOUND = new ErrorResult("S00007", "根据ID没有查询到对象");
  9. }
复制代码
1.3 通用异常类定义

定义一个通用的异常类 CommonException,继承自 RuntimeException,当程序中捕获到编译时异常或业务异常时,就抛出这个通用异常,交给全局来处理。(随着业务复杂度的增加,可以细分自定义异常,如 AuthException、UserException、CouponException 等,让这些细分异常都继承自 CommonException。)
com.yygnb.demo.common.exception.CommonException:
  1. @EqualsAndHashCode(callSuper = true)
  2. @Data
  3. public class CommonException extends RuntimeException {
  4.     protected ErrorResult errorResult;
  5.     public CommonException(String message) {
  6.         super(message);
  7.     }
  8.     public CommonException(String message, Throwable cause) {
  9.         super(message, cause);
  10.     }
  11.     public CommonException(Throwable cause) {
  12.         super(cause);
  13.     }
  14.     protected CommonException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
  15.         super(message, cause, enableSuppression, writableStackTrace);
  16.     }
  17.     public CommonException(String code, String msg) {
  18.         super(msg + "(" + code + ")");
  19.         this.errorResult = new ErrorResult(code, msg);
  20.     }
  21.     public CommonException(ErrorResult errorResult) {
  22.         super(errorResult.getMsg() + "(" + errorResult.getCode() + ")");
  23.         this.errorResult = errorResult;
  24.     }
  25. }
复制代码
这个自定义异常类复写了父类的构造函数,同时定义了一个成员变量 ErrorResult,便于在同一异常处理时快速构造返回信息。
1.4 全局异常处理

Spring MVC 中提供了全局异常处理的注解:@ControllerAdvice 和 @RestControllerAdvice。由于前后端分离开发 RESTful 接口,我们这里就使用 @RestControllerAdvice。
com.yygnb.demo.common.exception.CommonExceptionHandler:
  1. @Slf4j
  2. @RestControllerAdvice
  3. public class CommonExceptionHandler {
  4.     /**
  5.      * 通用业务异常
  6.      *
  7.      * @param e
  8.      * @return
  9.      */
  10.     @ExceptionHandler(value = CommonException.class)
  11.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  12.     public ErrorResult handleCommonException(CommonException e) {
  13.         log.error("{}, {}", e.getMessage(), e);
  14.         if (e.getErrorResult() != null) {
  15.             return e.getErrorResult();
  16.         }
  17.         return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
  18.     }
  19.     /**
  20.      * 其他运行时异常
  21.      * @param e
  22.      * @return
  23.      */
  24.     @ExceptionHandler(value = Exception.class)
  25.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  26.     public ErrorResult handleDefaultException(Exception e) {
  27.         log.error("{}, {}", e.getMessage(), e);
  28.         return DefaultErrorResult.SYSTEM_ERROR;
  29.     }
  30. }
复制代码
上面捕获了 CommonException 和 Exception,匹配顺序为从上往下。假设 handleDefaultException 在前面,当发生一个异常 CommonException 时,一来就会被 handleDefaultException 捕获,因为无论什么异常,都属于 Exception 的实例,不会执行 handleCommonException。所以越具体的异常处理,越要写在前面。
1.5 测试统一异常处理

在 DemoController 中抛出一个 CommonException:
  1. @GetMapping("hello")
  2. public String hello(String msg) {
  3.     String result = "Hello Spring Boot ! " + msg;
  4.     if ("demo".equals(msg)) {
  5.         throw new CommonException("发生错误----这是自定义异常");
  6.     }
  7.     return result;
  8. }
复制代码
启动服务,访问:
  1. http://localhost:9099/demo/hello?msg=demo
复制代码
结果返回:
  1. {
  2.   "code": "C99999",
  3.   "msg": "发生错误----这是自定义异常"
  4. }
复制代码
可以看出全局统一异常处理已经生效了。
2 参数校验

传统参数校验方式是通过多个 if/else 来进行,代码量大,很没有意义。Spring Boot 中有个 starter spring-boot-starter-validation 可以帮助咱们很方便的实现参数校验。
2.1 添加依赖

有些文章中说 spring boot 2.3 还是多少版本以后不用手动加入这个 starter,我试了以后不行,需要手动引入该依赖才行。
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>
复制代码
这个 starter 定义 Validator 以及 SmartValidator 接口,提供 @Validated,支持 spring 环境,支持验证组的规范, 提供了一系列的工厂类以及适配器。底层依赖 hibernate-validator 包。
2.2 完善异常处理类

在 1.4 中只捕获了 CommonException 和 Exception,此处要完善参数绑定、校验等异常。补充后 CommonExceptionHandler 如下:
  1. @Slf4j
  2. @RestControllerAdvice
  3. public class CommonExceptionHandler {
  4.     @ExceptionHandler(value = BindException.class)
  5.     @ResponseStatus(HttpStatus.BAD_REQUEST)
  6.     public ErrorResult handleBindException(BindException e) {
  7.         log.error("{}", e.getMessage(), e);
  8.         List<String> defaultMsg = e.getBindingResult().getAllErrors()
  9.                 .stream()
  10.                 .map(ObjectError::getDefaultMessage)
  11.                 .collect(Collectors.toList());
  12.         return ErrorResult.build(DefaultErrorResult.PARAM_BIND_ERROR, defaultMsg.get(0));
  13.     }
  14.     @ExceptionHandler(value = MethodArgumentNotValidException.class)
  15.     @ResponseStatus(HttpStatus.BAD_REQUEST)
  16.     public ErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
  17.         log.error("{}", e.getMessage(), e);
  18.         List<String> defaultMsg = e.getBindingResult().getFieldErrors()
  19.                 .stream()
  20.                 .map(fieldError -> "【" + fieldError.getField() + "】" + fieldError.getDefaultMessage())
  21.                 .collect(Collectors.toList());
  22.         return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
  23.     }
  24.     @ExceptionHandler(value = MissingServletRequestParameterException.class)
  25.     @ResponseStatus(HttpStatus.BAD_REQUEST)
  26.     public ErrorResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
  27.         log.error("{}", e.getMessage(), e);
  28.         log.error("ParameterName: {}", e.getParameterName());
  29.         log.error("ParameterType: {}", e.getParameterType());
  30.         return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, e.getMessage());
  31.     }
  32.     @ExceptionHandler(value = ConstraintViolationException.class)
  33.     @ResponseStatus(HttpStatus.BAD_REQUEST)
  34.     public ErrorResult handleBindGetException(ConstraintViolationException e) {
  35.         log.error("{}", e.getMessage(), e);
  36.         List<String> defaultMsg = e.getConstraintViolations()
  37.                 .stream()
  38.                 .map(ConstraintViolation::getMessage)
  39.                 .collect(Collectors.toList());
  40.         return ErrorResult.build(DefaultErrorResult.PARAM_VALID_ERROR, defaultMsg.get(0));
  41.     }
  42.     @ExceptionHandler(HttpMessageNotReadableException.class)
  43.     @ResponseStatus(HttpStatus.BAD_REQUEST)
  44.     public ErrorResult error(HttpMessageNotReadableException e){
  45.         log.error("{}", e.getMessage(), e);
  46.         return DefaultErrorResult.JSON_PARSE_ERROR;
  47.     }
  48.     /**
  49.      * 通用业务异常
  50.      *
  51.      * @param e
  52.      * @return
  53.      */
  54.     @ExceptionHandler(value = CommonException.class)
  55.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  56.     public ErrorResult handleCommonException(CommonException e) {
  57.         log.error("{}, {}", e.getMessage(), e);
  58.         if (e.getErrorResult() != null) {
  59.             return e.getErrorResult();
  60.         }
  61.         return new ErrorResult(DefaultErrorResult.CUSTOM_ERROR.getCode(), e.getMessage());
  62.     }
  63.     /**
  64.      * 其他运行时异常
  65.      * @param e
  66.      * @return
  67.      */
  68.     @ExceptionHandler(value = Exception.class)
  69.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  70.     public ErrorResult handleDefaultException(Exception e) {
  71.         log.error("{}, {}", e.getMessage(), e);
  72.         return DefaultErrorResult.SYSTEM_ERROR;
  73.     }
  74. }
复制代码
2.3 自定义校验注解

在 javax.validation 中提供了很多用于校验的注解,常见的如:@NotNull、@Min、@Max 等等,但可能这些注解不够,需要自定义注解。例如咱们自定义一个注解 @OneOf,该注解对应字段的值只能从 value 中选择:使用方式为:
  1. @OneOf(value = {"MacOS", "Windows", "Linux"})
复制代码
首先定义一个注解
com.yygnb.demo.common.validator.OneOf:
[code]@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documented@Constraint(validatedBy = OneOfValidator.class)public @interface OneOf {    String message() default "只能从备选值中选择";    Class[] groups() default {};    Class
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

冬雨财经

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表