参数校验架构validator与优雅的异常处理

概述

  • spring boot参数校验框架validator的使用以及优雅的异常处理方式。
  • 在spring boot项目中,为了让rest接口更加的稳定,健壮,避免非法参数造成的系统未知异常,因此对传入的参数进行校验是非常有必要的。接下来分享一下比较友好的校验方式。

校验框架[hibernate-validator]

介绍

  • 来自官网的介绍 Express validation rules in a standardized way using annotation-based constraints and benefit from transparent integration with a wide variety of frameworks.
  • 大概翻译过来就是,使用基于注解的标准化的式表达式验证规则,并受益于与各种框架的透明集成。
  • 自带的注解如下:
    在这里插入图片描述

spring boot集成

  • 1.maven添加依赖,如果spring boot在2.3.0以下,忽略此步骤,因为2.3.0以下的版本已经集成有validator模块
<!--spring boot 2.3.0以上需要手动引入-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 2.添加代码
//传输对象dto
@Data
public class DemoDTO implements Serializable {
    
    
    private static final long serialVersionUID = 1019466745376831818L;
    @NotNull(message = "字段a不允许为空")
    private Integer a;
    @NotBlank(message = "字段b不允许为空")
    private String b;
}

//统一结果返回
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Response<T> implements Serializable {
    
    
    private static final long serialVersionUID = 4921114729569667431L;
    //状态码,200为成功,其它为失败
    private Integer code;
    //消息提示
    private String message;
    //数据对象
    private T data;
    //成功状态码
    public static final int SUCCESS = 200;
    //失败状态码
    public static final int ERROR = 1000;
    public static <R> Response<R> success(R data) {
    
    
        return new Response<>(SUCCESS, "success", data);
    }
    public static <R> Response<R> error(String msg) {
    
    
        return new Response<>(ERROR, msg, null);
    }

}

//控制器
@RestController
public class DemoController {
    
    
    @PostMapping(value = "/check")
    public Response<Boolean> check(@Valid @RequestBody DemoDTO dto) {
    
    
        return Response.success(true);
    }
}
  • 3.启动服务,访问 http://localhost:8080/check
//请求参数:
{
    
    
    "a": null,
    "b": null
}

//返回结果:
{
    
    
    "timestamp": "2021-03-05T09:05:35.780+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/check"
}

说明能正常拦截,但是这个提示不友好,该如何优化呢?往下会具体说明。

自定义校验器

  • 1.默认的校验器,并不能满足所有的需求,比如我要限制传入的字符串长度在1到20之间,但是要求中文一个汉字按两个字符计算,即最多只能输入10个汉字,默认的@Size无法解决这问题;又比如,传入的参数只能是固定的枚举值,又该怎么办呢?所以,就需要到自定义注解出马了。
  • 2.添加代码
//字符长度校验注解(中文占两个字符)
@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = LengthValidator.class)
public @interface Length {
    
    
    boolean required() default true;
    String message() default "字符串长度不在范围内";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };
    int min() default 0;
    int max() default Integer.MAX_VALUE;
}

//字符长度校验器(中文占两个字符)
public class LengthValidator implements ConstraintValidator<Length, String> {
    
    
    private Length length;
    @Override
    public void initialize(Length constraintAnnotation) {
    
    
        this.length = constraintAnnotation;
    }
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
    
    
        int len = this.calLen(s);
        return len >= length.min() && len <= length.max();
    }
    private int calLen(String s) {
    
    
        if (null == s) {
    
    
            return 0;
        }
        //中文占两个字符长度
        return s.trim()
                .replaceAll("[^\\x00-\\xff]", "**")
                .length();
    }
}

//范围枚举值
@Target({
    
    METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = RangeEnumValidator.class)
public @interface RangeEnum {
    
    
    boolean required() default true;
    String message() default "枚举值错误";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };
    String[] allowableValues() default {
    
    };
}

//范围枚举值校验器
public class RangeEnumValidator implements ConstraintValidator<RangeEnum, Object> {
    
    
    private RangeEnum range;
    @Override
    public void initialize(RangeEnum constraintAnnotation) {
    
    
        this.range = constraintAnnotation;
    }
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
    
    
        String[] allowableValues = range.allowableValues();
        if (null == o || allowableValues.length == 0) {
    
    
            return false;
        }
        String s = o.toString();
        for (String allowableValue : allowableValues) {
    
    
            if (s.equals(allowableValue)) {
    
    
                return true;
            }
        }
        return false;
    }
}

//传输对象dto
@Data
public class Demo2DTO implements Serializable {
    
    
    private static final long serialVersionUID = 1019466745376831818L;
    @Length(min = 1, max = 5, message = "字段c长度在1到5个字符之间")
    private String c;
    @RangeEnum(allowableValues = {
    
    "x", "y"}, message = "字段d允许取值为:x,y")
    private String d;
}

//控制器
@RestController
public class DemoController {
    
    
    @PostMapping(value = "/check2")
    public Response<Boolean> check2(@Valid @RequestBody Demo2DTO dto) {
    
    
        return Response.success(true);
    }
}
  • 3.启动服务,访问 http://localhost:8080/check2
//请求参数:
{
    
    
    "c": null,
    "d": null
}

//返回结果:
{
    
    
    "timestamp": "2021-03-05T09:05:35.780+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/check2"
}

优雅的异常处理

  • 1.前面我们已经看到,参数非法,提示十分不友好,请求方根本不知道哪里出了问题,为了解决这个问题,我们需要使用到org.springframework.web.bind.annotation.RestControllerAdvice,此注解允许我们捕获全局通知,配合org.springframework.web.bind.annotation.ExceptionHandler拦截异常,并对异常做自定义处理,然后响应请求。
  • 2.添加代码
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    

    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Response validExceptionHandler(MethodArgumentNotValidException e) {
    
    
        print(e);
        StringBuilder buf = new StringBuilder();
        for (ObjectError error : e.getBindingResult().getAllErrors()) {
    
    
            buf.append(",").append(error.getDefaultMessage());
        }
        return Response.error(buf.length() > 0 ? buf.substring(1) : "参数校验失败");
    }

    /**
     * 处理Exceptionn异常
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public Response handleException(Exception e) {
    
    
        print(e);
        String message = e.getMessage();
        if (null == message || "".equals(message)) {
    
    
            message = e.getClass().getSimpleName();
        }
        return Response.error(message);
    }

    private void print(Exception e) {
    
    
        log.error(" Exception : {}", e.getClass().getSimpleName(), e);
    }
}

参数校验不通过,会抛出MethodArgumentNotValidException异常,我们通过添加@ExceptionHandler(MethodArgumentNotValidException.class)注解,即可以对参数校验异常进行处理

  • 3.启动服务,访问 http://localhost:8080/check2
//请求参数:
{
    
    
    "a": null,
    "b": null
}

//返回结果:
{
    
    
    "code": 1000,
    "message": "字段d允许取值为:x,y,字段c长度在1到5个字符之间",
    "data": null
}
  • 码云 https://gitee.com/hweiyu/spring-boot-validator.git

猜你喜欢

转载自blog.csdn.net/vbhfdghff/article/details/114394051