Springboot优雅参数校验,统一响应,异常处理

1.统一响应

(1)统一状态码
首先定义一个状态码接口,所有状态码都需要实现它

public interface StatusCode {
    public int getCode();
    public String getMsg();
}

枚举类实现接口

@Getter
public enum ResultCode implements StatusCode{
    SUCCESS(1000,"请求成功"),
    FAILED(1001,"请求失败"),
    VALIDATE_ERROR(1002,"参数校验失败"),
    RESPONSE_PACK_ERROR(1003,"response返回包装失败");

    private int code;
    private String msg;
    ResultCode(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
}

开始写 ResultVo 包装类了,我们预设了几种默认的方法,比如成功的话就默认传入 object 就可以了,我们自动包装成 success。

@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResultVo {
    private int code;
    private String msg;
    private Object data;

    public static ResultVo success(Object data){
        return new ResultVo(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMsg(),data);
    }
    public static ResultVo success(String msg,Object data){
        return new ResultVo(ResultCode.SUCCESS.getCode(),msg,data);
    }

    public static ResultVo fail(Integer code,String msg){
        return new ResultVo(code,msg,null);
    }
}

2.统一校验

@Validated 参数校验
有了 @Validated 我们只需要再 vo 上面加一点小小的注解,便可以完成校验功能

@Data
public class ProductInfoVo {
    @NotNull(message = "商品名称不允许为空")
    private String productName;

    @Min(value = 0, message = "商品价格不允许为负数")
    private BigDecimal productPrice;

    private Integer productStatus;
}
 @PostMapping("/findByVo")
    public ProductInfo findByVo(@Validated ProductInfoVo vo) {
        ProductInfo productInfo = new ProductInfo();
        BeanUtils.copyProperties(vo, productInfo);
        return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
    }

运行看看,如果参数不对会发生什么?

{
  "timestamp": "2020-04-19T03:06:37.268+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [
    {
      "codes": [
        "Min.productInfoVo.productPrice",
        "Min.productPrice",
        "Min.java.math.BigDecimal",
        "Min"
      ],
      "arguments": [
        {
          "codes": [
            "productInfoVo.productPrice",
            "productPrice"
          ],
          "defaultMessage": "productPrice",
          "code": "productPrice"
        },
        0
      ],
      "defaultMessage": "商品价格不允许为负数",
      "objectName": "productInfoVo",
      "field": "productPrice",
      "rejectedValue": -1,
      "bindingFailure": false,
      "code": "Min"
    }
  ],
}

并不是想要的返回结果

2.1分组校验

分组校验解决的问题:例如当对传来的DTO校验时,添加时id可以未空,但是修改时id不能为空。想要重复使用一个dto就遇到不能使用@NotNull进行校验,所以可以使用分组校验。
流程:
在common中新建valid包,里面新建两个空接口AddGroup,UpdateGroup用来分组
image
给校验注解,标注上groups,指定什么情况下才需要进行校验

如:指定在更新和添加的时候,都需要进行校验

@NotEmpty
@NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class})
private String name;

如果没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
业务方法参数上使用@Validated注解,并在value中给出group接口,标记当前校验是哪个组

@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
        ...
    }

2.2 自定义参数校验注解

项目中引入spring-boot-starter-validation的starter依赖

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
 </dependency>

hibernate-validator提供了很多校验器注解,例如@Email,参考可以自定义自己的注解
下面以日期格式校验规则为例:
(1)定义注解类

package com.validator.demo.api.valid;
 
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
import javax.validation.Constraint;
import javax.validation.Payload;
 
import org.apache.commons.lang3.StringUtils;
 
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { ValidDateValidator.class })
public @interface ValidDate {
    String pattern() default "yyyy-MM-dd";
 
    String message() default StringUtils.EMPTY;
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
}

@Constraint —— 表示我们判断逻辑的具体实现类是什么。
(2)定义逻辑实现类

package com.validator.demo.api.valid;
 
import java.text.ParseException;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
 
public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {
 
    private String pattern = StringUtils.EMPTY;
 
    @Override
    public void initialize(ValidDate constraintAnnotation) {
        pattern = constraintAnnotation.pattern();
    }
 
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (StringUtils.isBlank(value)) {
            return true;
        }
        try {
            DateUtils.parseDateStrictly(value, pattern);
        } catch (ParseException e) {
            return false;
        }
        return true;
    }
 
}

注意:ConstraintValidator<ValidDate, String> 第一个参数时注解,第二个参数时要校验的类型
(3)在实体类中添加相应的注解
image
(4)测试
image

3.优化异常处理

spring提供了一个 @RestControllerAdvice 来增强所有 @RestController,然后使用 @ExceptionHandler 注解,就可以拦截到对应的异常。

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler(value = Exception.class)
    public ResultVo MethodArgumentNotValidExceptionHandler(Exception e){
        return ResultVo.fail(ResultCode.VALIDATE_ERROR.getCode(),e.getMessage());
    }
}

4.统一异常

每个系统都会有自己的业务异常,比如库存不能小于 0 子类的,这种异常并非程序异常,而是业务操作引发的异常,我们也需要进行规范的编排业务异常状态码,并且写一个专门处理的异常类,最后通过刚刚学习过的异常拦截统一进行处理,以及打日志。
(1)异常状态码枚举,既然是状态码,那就肯定要实现我们的标准接口 StatusCode。

@Getter
public enum  AppCode implements StatusCode {

    APP_ERROR(2000, "业务异常"),
    PRICE_ERROR(2001, "价格异常");

    private int code;
    private String msg;

    AppCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

(2)异常类
异常类,这里需要强调一下,code 代表 AppCode 的异常状态码,也就是 2000;msg 代表业务异常,这只是一个大类,一般前端会放到弹窗 title 上;最后 super(message); 这才是抛出的详细信息,在前端显示在弹窗体中,在 ResultVo 则保存在 data 中。

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    // 手动设置异常
    public APIException(StatusCode statusCode, String message) {
        // message用于用户设置抛出错误详情,例如:当前价格-5,小于0
        super(message);
        // 状态码
        this.code = statusCode.getCode();
        // 状态码配套的msg
        this.msg = statusCode.getMsg();
    }

    // 默认异常使用APP_ERROR状态码
    public APIException(String message) {
        super(message);
        this.code = AppCode.APP_ERROR.getCode();
        this.msg = AppCode.APP_ERROR.getMsg();
    }

}

(3)最后进行统一异常的拦截,这样无论在 service 层还是 controller 层,开发人员只管抛出 API 异常,不需要关系怎么返回给前端,更不需要关心日志的打印。

@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler({BindException.class})
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        // 从异常对象中拿到ObjectError对象
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }

    @ExceptionHandler(APIException.class)
    public ResultVo APIExceptionHandler(APIException e) {
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}

最后使用,我们的代码只需要这么写。

if (null == orderMaster) {
            throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
        }

{
"code": 2003,
"msg": "订单不存在",
"data": "订单号不存在:1998"
}

posted @ 2022-06-29 09:07  spiderMan1-1  阅读(239)  评论(0编辑  收藏  举报