@Valid 和 @Validated 注解区别以及自定义参数验证器的使用方法以及原理解析以及自定义参数解析器

  在Web项目中经常需要验证前台的参数,比如验证param != null 或者验证param 的长度、集合的大小等等。一种办法就是手动验证,那就是写大量的if代码块,另一种就是使用现成的validation。

  @Valid 注解位于包 javax.validation; @Validated 注解位于包org.springframework.validation.annotation, 是Spring 提供的注解。

  @Validated是@Valid 的一次封装,是Spring提供的校验机制使用。@Valid不提供分组功能,而@Validated 提供分组的功能。

1. 引入相关依赖

        <!-- validate 相关注解 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.1.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.4.1.Final</version>
        </dependency>

2. 使用

1. 不带分组的使用:

接收前端参数的Bean:

package com.xm.ggn.test;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;

@Data
public class User2 implements Serializable {

    @NotNull(message = "username 不能为空")
    @Length(min = 2, max = 20, message = "username 长度必须在{min}到{max}之间")
    private String username;

    @NotNull(message = "age 不能为空")
    @Range(min = 18, max = 25, message = "年龄必须在{min}-{max}之间")
    private Integer age;

    /**
     * 爱好
     */
    @NotEmpty(message = "爱好不能为空")
    private List<String> hobbies;

    private String fullname;
}

两个测试Controller:

    @PostMapping("/user/add2")
    public User2 addUser2(@RequestBody @Valid User2 user) {
        System.out.println(user);
        return user;
    }

    @PostMapping("/user/add3")
    public User2 addUser3(@RequestBody @Validated User2 user) {
        System.out.println(user);
        return user;
    }

全局异常处理器: (捕捉到上面接口参数验证失败抛出的异常,然后提取到错误信息返回给前端)

package com.xm.ggn.exception;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.val;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.xm.ggn.utils.JSONResultUtil;
import com.xm.ggn.utils.constant.ErrorCodeDefine;

import lombok.extern.slf4j.Slf4j;

/**
 * 全局异常处理器
 * 
 * @author Administrator
 *
 */
@RestControllerAdvice
@Slf4j
public class BXExceptionHandler {

    @ExceptionHandler(value = Throwable.class)
    public JSONResultUtil<Object> errorHandler(HttpServletRequest reqest, HttpServletResponse response, Exception e) {
        log.error("MyExceptionHandler errorHandler", e);

        // token错误
        /*
         * if (e instanceof InvalidAccessTokenException) {
         * InvalidAccessTokenException exception = (InvalidAccessTokenException)
         * e; return JSONResultUtil.errorWithMsg("u100004",
         * exception.getMessage()); }
         */

        // SpringMVC映射的参数没传值,导致映射参数失败
        if (e instanceof HttpMessageNotReadableException) {
            return JSONResultUtil.error("必传参数为空");
        }

        // @Valid 参数验证失败错误信息解析
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            BindingResult bindingResult = exception.getBindingResult();
            int errorCount = bindingResult.getErrorCount();
            if (errorCount > 0) {
                String defaultMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
                return JSONResultUtil.error(defaultMessage);
            }
            String message = exception.getMessage();
            return JSONResultUtil.error(message);
        }

        if (e instanceof HttpRequestMethodNotSupportedException) {
            return JSONResultUtil.error("u100001");
        }

        // 代码用ValidateUtils 工具类进行参数校验抛出的异常
        if (e instanceof BxIllegalArgumentException) {
            BxIllegalArgumentException exception = (BxIllegalArgumentException) e;
            return JSONResultUtil.errorWithMsg(exception.getErrorCode(), exception.getMessage());
        }

        return JSONResultUtil.error(ErrorCodeDefine.SYSTEM_ERROR);
    }
}

测试Curl:

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz"}' http://localhost:8088//user/add2                        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    94    0    76  100    18   6909   1636 --:--:-- --:--:-- --:--:--  9400{"success":false,"data":null,"msg":"age 不能为空","errorCode":"u100000"}

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 90}' http://localhost:8088//user/add3             % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   115    0    86  100    29   3909   1318 --:--:-- --:--:-- --:--:--  5476{"success":false,"data":null,"msg":"年龄必须在18-25之间","errorCode":"u100000"}

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add3
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   173    0   119  100    54  11900   5400 --:--:-- --:--:-- --:--:-- 19222{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null},"msg":"成功","errorCode":"0"}

2  测试分组的使用-分组只能针对Spring 提供的注解Validated 

  带分组的功能是说可以在@Validated 注解可以声明验证的指定的分组。

1.  修改接收对象的实体如下

package com.xm.ggn.test;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;

@Data
public class User2 implements Serializable {

    @NotNull(message = "username 不能为空")
    @Length(min = 2, max = 20, message = "username 长度必须在{min}到{max}之间")
    private String username;

    @NotNull(message = "age 不能为空")
    @Range(min = 18, max = 25, message = "年龄必须在{min}-{max}之间")
    private Integer age;

    /**
     * 爱好
     */
    @NotEmpty(message = "爱好不能为空")
    private List<String> hobbies;

    private String fullname;

    /**
     * 密码,只在新增的时候进行验证
     */
    @NotNull(message = "password 不能为空", groups = {AddUser.class})
    @Length(min = 6, max = 20, message = "password 长度必须在{min}到{max}之间", groups = {AddUser.class})
    private String password;

    /**
     * 内部累标记是新增操作
     */
    public static interface AddUser {
    }
}

  这里需要注意groups 声明的class 必须是接口类型。

2. 修改Controller

    @PostMapping("/user/add3")
    public User2 addUser3(@RequestBody @Validated User2 user) {
        System.out.println(user);
        return user;
    }

    @PostMapping("/user/add5")
    public User2 addUser5(@RequestBody @Validated(User2.AddUser.class) User2 user) {
        System.out.println(user);
        return user;
    }

3. 测试

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add3
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   189    0   135  100    54  10384   4153 --:--:-- --:--:-- --:--:-- 15750{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null,"password":null},"msg":"成功","errorCode":"0"}

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add5
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   135    0    81  100    54   8100   5400 --:--:-- --:--:-- --:--:-- 13500{"success":false,"data":null,"msg":"password 不能为空","errorCode":"u100000"}

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"password": "111222"}' http://localhost:8088//user/add5
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   156    0   134  100    22   8375   1375 --:--:-- --:--:-- --:--:-- 10400{"success":true,"data":{"username":null,"age":null,"hobbies":null,"fullname":null,"password":"111222"},"msg":"成功","errorCode":"0"}

  可以看出这里如果@Validated 指定了class 会只验证指定class 分组的信息,如果不指定会验证所有不带组号的规则。

补充:其实@Valid 相当于不带属性的@Validated, 默认也是验证不带 groups 属性的验证规则

比如测试如下:

(1) 接受参数的Bean

package com.xm.ggn.test;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;

import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;

@Data
public class User2 implements Serializable {

    @NotNull(message = "username 不能为空")
    @Length(min = 2, max = 20, message = "username 长度必须在{min}到{max}之间")
    private String username;

    @NotNull(message = "age 不能为空")
    @Range(min = 18, max = 25, message = "年龄必须在{min}-{max}之间")
    private Integer age;

    /**
     * 爱好
     */
    @NotEmpty(message = "爱好不能为空")
    private List<String> hobbies;

    private String fullname;

    /**
     * 密码,只在新增的时候进行验证
     */
    @NotNull(message = "password 不能为空", groups = {AddUser.class})
    @Length(min = 6, max = 20, message = "password 长度必须在{min}到{max}之间", groups = {AddUser.class})
    private String password;

    /**
     * 内部累标记是新增操作
     */
    public static interface AddUser {
    }
}

(2) Controller

    @PostMapping("/user/add2")
    public User2 addUser2(@RequestBody @Valid User2 user) {
        System.out.println(user);
        return user;
    }

(3) curl 测试

$ curl -X POST --header 'Content-Type: application/json' -d '{"username": "zz", "age": 19, "hobbies": ["lq", "zq"]}' http://localhost:8088//user/add2
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   189    0   135  100    54    182     73 --:--:-- --:--:-- --:--:--   255{"success":true,"data":{"username":"zz","age":19,"hobbies":["lq","zq"],"fullname":null,"password":null},"msg":"成功","errorCode":"0"}

  可以看出password 属性验证规则带有groups 属性,没有被验证到。 

2. @Valid、@Validated 参数验证原理

org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument 参数解析过程中进行验证

    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }

        return adaptArgumentIfNecessary(arg, parameter);
    }

validateIfApplicable(binder, parameter); 是进行参数校验,并且将校验结果收集到binder.getBindingResult() 中。

if 语句进行判断如果有验证不通过的,并且 isBindExceptionRequired 方法判断是否需要抛出异常。其判断逻辑如下:

    protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
        int i = parameter.getParameterIndex();
        Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
        boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
        return !hasBindingResult;
    }

如果当前验证参数的下一个参数是Errors 的子类则不抛出异常,异常会封装到 Errors 的子类中。  如果下一个参数不是Errors 的子类,则走上面抛出异常的代码 throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());所以看到这里可以发现有两种处理方式:

第一种是用 BindingResult bindingResult 接受错误结果;

    @PostMapping("/user/add4")
    public User2 addUser4(@RequestBody @Validated User2 user, BindingResult bindingResult) {
        System.out.println(bindingResult);
        System.out.println(user);
        return user;
    }

第二种是 增加全局异常拦截器,拦截上面的异常,然后给前端返回对应的错误信息:

Controller:

    @PostMapping("/user/add3")
    public User2 addUser3(@RequestBody @Validated User2 user) {
        System.out.println(user);
        return user;
    }

全局异常拦截器:

package com.xm.ggn.exception;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import lombok.val;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.xm.ggn.utils.JSONResultUtil;
import com.xm.ggn.utils.constant.ErrorCodeDefine;

import lombok.extern.slf4j.Slf4j;

/**
 * 全局异常处理器
 * 
 * @author Administrator
 *
 */
@RestControllerAdvice
@Slf4j
public class BXExceptionHandler {

    @ExceptionHandler(value = Throwable.class)
    public JSONResultUtil<Object> errorHandler(HttpServletRequest reqest, HttpServletResponse response, Exception e) {
        log.error("MyExceptionHandler errorHandler", e);

        // token错误
        /*
         * if (e instanceof InvalidAccessTokenException) {
         * InvalidAccessTokenException exception = (InvalidAccessTokenException)
         * e; return JSONResultUtil.errorWithMsg("u100004",
         * exception.getMessage()); }
         */

        // SpringMVC映射的参数没传值,导致映射参数失败
        if (e instanceof HttpMessageNotReadableException) {
            return JSONResultUtil.error("必传参数为空");
        }

        // @Valid 参数验证失败错误信息解析
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            BindingResult bindingResult = exception.getBindingResult();
            int errorCount = bindingResult.getErrorCount();
            if (errorCount > 0) {
                String defaultMessage = bindingResult.getAllErrors().get(0).getDefaultMessage();
                return JSONResultUtil.error(defaultMessage);
            }
            String message = exception.getMessage();
            return JSONResultUtil.error(message);
        }

        if (e instanceof HttpRequestMethodNotSupportedException) {
            return JSONResultUtil.error("u100001");
        }

        // 代码用ValidateUtils 工具类进行参数校验抛出的异常
        if (e instanceof BxIllegalArgumentException) {
            BxIllegalArgumentException exception = (BxIllegalArgumentException) e;
            return JSONResultUtil.errorWithMsg(exception.getErrorCode(), exception.getMessage());
        }

        return JSONResultUtil.error(ErrorCodeDefine.SYSTEM_ERROR);
    }
}

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable 方法进行验证, 这个验证过程会将验证的结果收集到binder.getBindingResult()。所以核心的逻辑是在这个方法内部。这个方法里面首先拿注解Validated 或者 判断注解是否是以Valid 开始。 这里也就确定了上面两个注解 @Valid 和 @Validated 都会被验证。

    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                binder.validate(validationHints);
                break;
            }
        }
    }

  这个方法里面首先拿注解Validated 或者 判断注解是否是以Valid 开始。 这里也就确定了上面两个注解 @Valid 和 @Validated 都会被验证。

  然后其核心逻辑会调用到: org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree#validateConstraints(org.hibernate.validator.internal.engine.ValidationContext<T>, org.hibernate.validator.internal.engine.ValueContext<?,V>, java.util.Set<javax.validation.ConstraintViolation<T>>)

    private <T, V> void validateConstraints(ValidationContext<T> validationContext,
            ValueContext<?, V> valueContext,
            Set<ConstraintViolation<T>> constraintViolations) {
        CompositionResult compositionResult = validateComposingConstraints(
                validationContext, valueContext, constraintViolations
        );

        Set<ConstraintViolation<T>> localViolations;

        // After all children are validated the actual ConstraintValidator of the constraint itself is executed
        if ( mainConstraintNeedsEvaluation( validationContext, constraintViolations ) ) {

            if ( log.isTraceEnabled() ) {
                log.tracef(
                        "Validating value %s against constraint defined by %s.",
                        valueContext.getCurrentValidatedValue(),
                        descriptor
                );
            }

            // find the right constraint validator
            ConstraintValidator<A, V> validator = getInitializedConstraintValidator( validationContext, valueContext );

            // create a constraint validator context
            ConstraintValidatorContextImpl constraintValidatorContext = new ConstraintValidatorContextImpl(
                    validationContext.getParameterNames(),
                    validationContext.getTimeProvider(),
                    valueContext.getPropertyPath(),
                    descriptor
            );

            // validate
            localViolations = validateSingleConstraint(
                    validationContext,
                    valueContext,
                    constraintValidatorContext,
                    validator
            );

            // We re-evaluate the boolean composition by taking into consideration also the violations
            // from the local constraintValidator
            if ( localViolations.isEmpty() ) {
                compositionResult.setAtLeastOneTrue( true );
            }
            else {
                compositionResult.setAllTrue( false );
            }
        }
        else {
            localViolations = Collections.emptySet();
        }

        if ( !passesCompositionTypeRequirement( constraintViolations, compositionResult ) ) {
            prepareFinalConstraintViolations(
                    validationContext, valueContext, constraintViolations, localViolations
            );
        }
    }

下面这行代码会获取到合适的验证器ConstraintValidator, 每个验证注解都有对应的validator 与之对应。

ConstraintValidator<A, V> validator = getInitializedConstraintValidator( validationContext, valueContext ); 寻找合适的validater,比如对于 @NotNull 获取到对应的Validator 是org.hibernate.validator.internal.constraintvalidators.bv.NotNullValidator

最后请求到达: org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#getInitializedValidator    方法内部从缓存中没有获取到相关的信息,然后调用下面方法进行获取

    org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager#createAndInitializeValidator    创建和初始化相关的validator
    然后将validator 放到缓存中
    最后调用 javax.validation.ConstraintValidator#isValid 进行验证。

org.hibernate.validator.internal.metadata.core.ConstraintHelper#ConstraintHelper 构造里面维护了验证注解与验证器的关系, 会在容器启动过程中进行调用然后维护其关系

3. 自定义自己的Validator 

  模仿org.hibernate.validator.internal.constraintvalidators.hv.LengthValidator 进行书写

(1) 定义注解

package com.xm.ggn.test.contraint;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

    String message() default "非法的数字";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int min() default 0;

    int max() default Integer.MAX_VALUE;
}

(2) 编写验证器

package com.xm.ggn.test.contraint;

import org.hibernate.validator.internal.util.logging.Log;
import org.hibernate.validator.internal.util.logging.LoggerFactory;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class NumberValidator implements ConstraintValidator<Number, Integer> {

    private static final Log log = LoggerFactory.make();

    private int min;
    private int max;

    @Override
    public void initialize(Number parameters) {
        min = parameters.min();
        max = parameters.max();
        validateParameters();
    }

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) {
            return true;
        }
        return value >= min && value <= max;
    }

    private void validateParameters() {
        if (min < 0) {
            throw log.getMinCannotBeNegativeException();
        }
        if (max < 0) {
            throw log.getMaxCannotBeNegativeException();
        }
        if (max < min) {
            throw log.getLengthCannotBeNegativeException();
        }
    }
}

(3) 测试

package com.xm.ggn.test;

import com.xm.ggn.test.contraint.Number;
import lombok.Data;

import javax.validation.constraints.NotNull;
import java.io.Serializable;

@Data
public class User3 implements Serializable {

    @NotNull(message = "age 不能为空")
    @Number(min = 18, max = 25, message = "年龄必须在{min}-{max}之间")
    private Integer age;
}

测试Controller

    @PostMapping("/user/add6")
    public User3 addUser6(@RequestBody @Validated User3 user) {
        System.out.println(user);
        return user;
    }

curl 测试:

qiaoliqiang@A022296-NC01 MINGW64 /e/xiangmu/bs-media (master)
$ curl -X POST --header 'Content-Type: application/json' -d '{"age": 2}' http://localhost:8088//user/add6
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    96    0    86  100    10    174     20 --:--:-- --:--:-- --:--:--   194{"success":false,"data":null,"msg":"年龄必须在18-25之间","errorCode":"u100000"}