spring boot 2.1学习笔记【十九】使用spring validation实现全局参数校验

springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;

文章目录

  • 欢迎关注本人公众号
  • 概述
  • 常用注解
  • 实例演示
  • 全局统一处理参数异常
  • 自定义注解校验
  • 对非bean的方法参数进行校验
  • 运行测试

欢迎关注本人公众号

spring boot 2.1学习笔记【十九】使用spring validation实现全局参数校验_第1张图片

概述

本文介绍在Spring Boot中实现对controller请求的数据进行全局校验。

  • JSR303/JSR-349: JSR303是一项标准,只提供规范不提供实现,规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。JSR-349是其的升级版本,添加了一些新特性。
  • hibernate validation:hibernate validation是对这个规范的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等
  • spring validation:spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中

本文核心为普通GET/POST请求参数校验;BEAN的嵌套校验;集合校验;全局同意异常处理。

常用注解

@Valid	被注释的元素是一个对象,需要检查此对象的所有字段值
@Null	被注释的元素必须为 null
@NotNull	被注释的元素必须不为 null
@AssertTrue	被注释的元素必须为 true
@AssertFalse	被注释的元素必须为 false
@Min(value)	被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)	被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)	被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)	被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)	被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)	被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past	被注释的元素必须是一个过去的日期
@Future	被注释的元素必须是一个将来的日期
@Pattern(value)	被注释的元素必须符合指定的正则表达式
@Email	被注释的元素必须是电子邮箱地址
@Length(min=, max=)	被注释的字符串的大小必须在指定的范围内
@NotEmpty	被注释的字符串的必须非空
@Range(min=, max=)	被注释的元素必须在合适的范围内
@NotBlank	被注释的字符串的必须非空
@URL(protocol=,host=,    port=, regexp=, flags=)	被注释的字符串必须是一个有效的url

主要区分下@NotNull @NotEmpty @NotBlank 3个注解的区别:

  • @NotNull 任何对象的value不能为null
  • @NotEmpty 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
  • @NotBlank 只能用于字符串不为null,并且字符串trim()以后length要大于0

实例演示

首先是两controller,两个方法,用于校验get和post请求。其实是一样的,只需要添加@Validated即可。

@RestController("validateController")
@Slf4j
public class ValidateController {

    @RequestMapping(value = "posttest", method = RequestMethod.POST)
    public String test1(@Validated @RequestBody StudentVO studentVO) {
        return studentVO.getName();
    }

    @RequestMapping(value = "gettest", method = RequestMethod.GET)
    public String test2(@Validated FatherVO father) {
        return father.getName();
    }
}

入参VO , 其中包含了普通校验;嵌套校验;list集合校验

@Data
@NoArgsConstructor
public class StudentVO {
    @NotBlank(message = "name不可为空")
    private String name;
    @Range(min = 0, max = 100, message = "年龄必须再[0-100]之间")
    private int age;
    @Email(message="非法邮件地址")
    private String email;
    @Pattern(regexp="^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$",message="身份证号不合规")
    private String cardNo;

    //自定义注解验证参数
    @Money
    private Double balance;

    @NotNull(message = "father不可为空")
    @Valid // 嵌套校验
    private FatherVO fatherVO;

    @NotEmpty       
    @Size(min = 1, max = 3, message = "fatherVOS长度必须在[1-3]之间")
    @Valid//list也可以嵌套校验
    private List<FatherVO> fatherVOS;
}

@Data
@NoArgsConstructor
public class FatherVO {
    @NotBlank(message = "父亲名字不可为空")
    private String name;
    @NotBlank(message = "父亲工作不可为空")
    private String work;
}

全局统一处理参数异常

这一步是关键,因为仅仅校验,提示信息可能不够友好,且异常信息也有很多种,所以我这里提供了统一处理。
更重要的是统一了错误返回码,对于接口测试开发很有用。

/**
 * 参数校验统一处理类
 */
@ControllerAdvice
@Slf4j
public class RestApiExceptionAdvice {

    @ExceptionHandler({BindException.class})
    @ResponseBody
    public R handleValidationException(BindException e) {
        StringBuilder bindErrorBuilder = new StringBuilder();
        StringBuilder noBindErrorBuilder = new StringBuilder();
        List<FieldError> fieldErrors = e.getFieldErrors();
        fieldErrors.forEach(fieldError -> {
            if (fieldError.isBindingFailure()) {
                bindErrorBuilder.append(fieldError.getField()).append(",");
            } else {
                noBindErrorBuilder.append(fieldError.getDefaultMessage()).append(",");
            }
        });
        String bindError = bindErrorBuilder.toString();
        String bindErrorMsg = StringUtils.isBlank(bindError) ? null : "参数有误:" + bindError.substring(0, bindError.length() - 1);
        String noBindError = noBindErrorBuilder.toString();
        String noBindErrorMsg = StringUtils.isBlank(noBindError) ? null : noBindError.substring(0, noBindError.length() - 1);

        String msg = StringUtils.isBlank(bindErrorMsg) ?
                noBindErrorMsg :
                bindErrorMsg + (StringUtils.isBlank(noBindErrorMsg) ? "" : "," + noBindErrorMsg);
        log.warn("参数校验不通过,msg: {}", msg, e);
        return new R<>(ParamErrorCode.PARAM_ERROR, msg, null);
    }

    @ExceptionHandler({ConstraintViolationException.class,
            MethodArgumentNotValidException.class,
            ServletRequestBindingException.class,
            MethodArgumentTypeMismatchException.class,
            IllegalArgumentException.class,
            HttpMessageNotReadableException.class})
    @ResponseBody
    public R handleValidationException(Exception e) {
        String msg = "";
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException t = (MethodArgumentNotValidException) e;
            msg = t.getBindingResult().getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .collect(Collectors.joining(","));
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException t = (ConstraintViolationException) e;
            msg = t.getConstraintViolations().stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(","));
        } else if (e instanceof MissingServletRequestParameterException) {
            MissingServletRequestParameterException t = (MissingServletRequestParameterException) e;
            msg = t.getParameterName() + " 不能为空";
        } else if (e instanceof MissingPathVariableException) {
            MissingPathVariableException t = (MissingPathVariableException) e;
            msg = t.getVariableName() + " 不能为空";
        } else if (e instanceof IllegalArgumentException) {
            IllegalArgumentException t = (IllegalArgumentException) e;
            msg = t.getMessage();
        } else {
            msg = "参数有误";
        }

        log.warn("参数校验不通过,msg: {}", msg, e);

        return new R<>(ParamErrorCode.PARAM_ERROR, msg, null);
    }

    /**
     * 统一拦截所有服务端抛出的异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public R handleException(Exception e) {
        log.error("服务器发生异常!", e);
        return new R<>(ParamErrorCode.ERROR, "服务器发生异常", e.getMessage());
    }
}



public class R<T> {

    private Integer code;

    private String msg;

    private T data;

    public R() {

    }

    public R(ParamErrorCode returnCode, String msg, T data) {
        this.code = returnCode.getCode();
        this.msg = msg;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

public enum ParamErrorCode {
    OK(200),
    PARAM_ERROR(100),
    BIZ_ERROR(300),
    ERROR(500),
    ;
    private Integer code;

    ParamErrorCode(Integer code) {
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }
}

自定义注解校验

有时候默认的一些校验规则,可能无法满足我们复杂的业务需求。此时可以通过自定义注解的方式来实现参数的校验。

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MoneyValidator.class)
public @interface Money {

    String message() default "不是金额形式";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}


public class MoneyValidator implements ConstraintValidator<Money, Double> {
   
    private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式  
    private Pattern moneyPattern = Pattern.compile(moneyReg);  
     
    public void initialize(Money money) {  
        
    }  
   
    public boolean isValid(Double value, ConstraintValidatorContext arg1) {  
       if (value == null)  
           return true;  
       return moneyPattern.matcher(value.toString()).matches();  
    }  
   
}

在VO中直接使用即可

//自定义注解验证参数
@Money
private Double balance;

对非bean的方法参数进行校验

上面都是将参数封装为Bean的形式传参,然后进行校验。有时候参数比较少,直接在方法参数传递:

@RequestMapping(value = "nomal", method = RequestMethod.GET)
    public String test3(@NotBlank(message = "name 不可为空") String name,
                        @NotNull(message = "age不可为空") @Min(value = 2, message = "最小2") Integer age) {
        return name;
    }

这种默认情况下,是无法参与检验的。不过好在hibernate validation提供了对方法参数的校验:https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/#section-validating-executable-constraints。
根据官方文档,只需要写个代理类进行处理即可

@Component
@Aspect
public class RequestParamValidAspect {
    @Pointcut("execution(* com.example.controller.*.*(..))")
    public void controllerBefore() {
    }

    @Before("controllerBefore()")
    public void before(JoinPoint point) {
        Object target = point.getThis();
        // 获得切入方法参数
        Object[] args = point.getArgs();
        // 获得切入的方法
        Method method = ((MethodSignature) point.getSignature()).getMethod();

        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        ExecutableValidator executableValidator = factory.getValidator().forExecutables();

        // 执行校验,获得校验结果
        Set<ConstraintViolation<Object>> validResult = executableValidator.validateParameters(target, method, args);
        //如果有校验不通过的
        if (!validResult.isEmpty()) {
            StringBuilder builder = new StringBuilder();
            validResult.forEach(objectConstraintViolation -> {
                builder.append(objectConstraintViolation.getMessage()).append(";");
            });
            String result = builder.toString();
            throw new IllegalArgumentException(result.substring(0, result.length() - 1));
        }
    }
}

运行测试

spring boot 2.1学习笔记【十九】使用spring validation实现全局参数校验_第2张图片
spring boot 2.1学习笔记【十九】使用spring validation实现全局参数校验_第3张图片
spring boot 2.1学习笔记【十九】使用spring validation实现全局参数校验_第4张图片

springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;

你可能感兴趣的:(spring,boot,2.X/spring,cloud,Greenwich,spring,boot)