Spring Validation

frameSpring

# 一、校验框架

  • Jakarta Bean Validation
  • hibernate-validator
  • spring-boot-starter-validation

# 1.1 Bean Validation

Jakarta Bean Validation (opens new window)

Jakarta Bean Validation API 3.0.0 (opens new window)

Jakarta Bean Validation是JCP通过的(JSR380)规范。

Jakarta有一个项目Bean Validation,用于对输入参数进行校验。

javax.validation是Jakarta Bean Validation中定义的一些规范。

通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。

# 1.2 Hibernate validator

Hibernate Validator (opens new window)

Hibernate Validator是对Bean Validation的实现,并增加了校验注解如@Email@Length等。

Hibernate Validator是目前使用最广泛的validation实现。

Maven方式引入:

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>7.0.1.Final</version>
</dependency>
1
2
3
4
5

Hibernate Validator已经更新到7.0.1.Final,对应的Jakarta Bean Validation规范版本为3.0.0

Maven仓库搜索:hibernate validator (opens new window)

# 1.3 Spring Validation

Spring为Validation提供了一个starter: spring-boot-starter-validation

Spring Validation是对hibernate validatior的二次封装,用于支持spring mvc参数自动校验。

# 二、Spring Validation

# 2.1 引入

如果spring-boot版本小于2.3.xspring-boot-starter-web会自动传入hibernate-validator依赖。

如果spring-boot版本大于2.3.x,则需要在 pom.xml中手动引入依赖。

Maven方式引入:

<!-- Spring扩展的数据校验器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1
2
3
4
5

正常情况下不需要添加版本号,Spring Boot 框架会在parent pom中指定版本,如果需要添加指定版本的jar包,可以在这里 (opens new window)查找。

当然你也可以直接使用其他的Validation实现,比如 [hibernate validator] 。

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>7.0.1.Final</version>
</dependency>
1
2
3
4
5

如果引入了 hibernate validator ,就可以不用引入 spring-boot-starter-validation 实现。

# 2.2 参数校验

# 1. requestBody参数校验

POSTPUT请求一般会使用requestBody传递参数,这种情况下,后端使用DTO对象进行接收。

只要给DTO对象加上@Validated注解就能实现自动参数校验。

如果校验失败,会抛出MethodArgumentNotValidException异常,Spring默认会将其转为400(Bad Request)请求。

示例:

在DTO字段上声明约束注解

比如,有一个保存User的接口,要求userName长度是2-10,account字段长度是6-20。

@Data
public class UserDTO {

    private Long userId;

    @NotBlank
    @Length(min = 2, max = 10)
    private String userName;

    @NotBlank
    @Length(min = 6, max = 20)
    private String account;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在方法参数上声明校验注解

这种情况下,使用@Valid@Validated都可以。

@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}
1
2
3
4
5

# 2. requestParam/PathVariable参数校验

GET请求一般会使用requestParam/PathVariable传参。

如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。

在这种情况下,必须在Controller类上标注@Validated注解,或者在入参前标注@Validated注解;并在入参上声明约束注解(如@Min等)。

如果校验失败,会抛出ConstraintViolationException异常。

示例:

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // 路径变量
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(1L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        return Result.ok(userDTO);
    }
}

public class UserController2 {
    // 查询参数
    @GetMapping("getByAccount")
    public Result getByAccount(@Validated @Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        return Result.ok(userDTO);
    }   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2.3 注解使用

# @Valid 、@Validated

在controller参数上增加,表示被标注的方法开启validation校验;

@Valid 和@Validated效果一样,可以加在controller中,也可以加载dto上。

如果这个请求的参数不符合实体DTO中定义的validation要求,spring就会抛出异常。比如用户名超长的情况,我们可以在统一的异常拦截器对异常进行捕获处理返回给用户。

@PostMapping()
public ResponseResult insert(@Validated(value = {Add.class}) @RequestBody T entity) {
}
1
2
3

# @NotNull、@NotEmpty 、@NotBlank

  • @NotNull: 不能为null,但可以为empty。可用在CharSequence、Collection、Map or Array object上。
  • @NotBlank: 只能作用在String上,不能为null,而且调用trim()后,长度必须大于0("test") 即必须有实际字符。
  • @NotEmpty:不能为null,而且长度必须大于0。 可用在CharSequence、Collection、Map or Array object上。

使用示例

public class User{
	@NotNull(message = "用户ID不能为空")
	private Long id;

	@NotBlank(message = "名称不为空")
	private String name;
    
	@NotEmpty (message = "角色集合不能为空")
	private List<String> roles;
}
1
2
3
4
5
6
7
8
9
10

校验结果对比

String name=null;
@NotNull:false
@NotEmpty:false
@NotBlank:false

String name="";
@NotNull:true
@NotEmpty:false
@NotBlank:false

String name=" ";
@NotNull:true
@NotEmpty:true
@NotBlank:false

String name="Greatanswer";
@NotNull:true
@NotEmpty:true
@NotBlank:true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# @Size

  • @Size(max,min) :限制字符长度必须在min到max之间
@Size(max = 20, message = "xxx最大长度为20个字符", groups = {Add.class, Update.class})
private String name;
1
2

# 其他注解

常见的注解可以在javax.validation.constraints包中看到。

  1. @Null 被注释的元素必须为null。
  2. @NotNull 被注释的元素不能为null 。
  3. @NotEmpty 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) 。
  4. @NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty, @NotBlank只应用于字符串且在比较时会去除字符串的空格。
  5. @AssertTrue 被注释的元素必须为true。
  6. @AssertFalse 被注释的元素必须为false 。
  7. @Min(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最小值 。
  8. @Max(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最大值。
  9. @Size(max,min) 被注释的元素的大小必须在指定的范围内。
  10. @Length 被注释的字符串的大小必须在指定的范围内。
  11. @Range 被注释的元素必须在合适的范围内。
  12. @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的 最小值。
  13. @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的 最大值。
  14. @Digits(integer,fraction) 被注释的元素必须是一个数字,其值必须在可接 受的范围内,限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过 fraction。
  15. @Past 被注释的元素必须是一个过去的日期。
  16. @Future 被注释的元素必须是一个将来的日期。
  17. @Pattern(regexp) 被注释的元素必须符合指定的正则表达式。
  18. @Email 被注释的元素必须是电子邮件地址。

# 2.4 分组

在实际项目中,可能多个方法需要使用同一个DTO类来接收参数,而不同方法的校验规则很可能是不一样的。

这个时候,简单地在DTO类的字段上加约束注解无法解决这个问题。

因此,spring-validation支持了分组校验的功能,专门用来解决这类问题。

@Validated校验与groups分组的关系:

@Validated注解校验可以添加对应分组的参数(group),这样可以只取参数中对应分组的字段进行验证。

分组

自定义校验器分组

/**
 * 校验器分组-添加,它只是一个标记类,不需要有任何方法
 */
public interface Add {
}

public interface Update {
}
1
2
3
4
5
6
7
8

DTO对象

保存User的时候,id是可空的,但是更新Role的时候,id的值不为空。

public class User {
    /**
    * 主键ID
    */
    @NotNull(message = "主键ID不能为空", groups = {Update.class})
    private Long id;
    
    /**
    * 名称
    */
    @NotBlank(message = "名称不能为空", groups = {Add.class, Update.class})
    @Size(max = 20, message = "名称最大长度为20个字符", groups = {Add.class, Update.class})
    private String name;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Controller

@Validated注解上指定校验分组。

@PostMapping()
public ResponseResult insert(@Validated(value = {Add.class}) @RequestBody T entity) {
}

@PutMapping()
public ResponseResult update(@Validated(value = {Update.class}) @RequestBody T entity) {
}
1
2
3
4
5
6
7

如果注解参数@Validated(Add.class),那么只会取参数中Add.class分组的字段进行验证;

反之,如果是@Validated(Update.class),那么就只会取被分组到Update.class组的字段进行验证。

# 2.5 嵌套校验

前面的示例中,DTO类里面的字段都是基本数据类型和String类型。

但是实际场景中,有可能某个字段也是一个对象,这种情况下可以使用嵌套校验。此时DTO类的对应字段(嵌套对象)必须标记@Valid注解。

示例:

比如,上面保存User信息的时候同时还带有Job信息。

@Data
public class User {
    // 主键ID
    @NotNull(message = "主键ID不能为空", groups = {Update.class})
    private Long id;
    
    // 名称
    @NotBlank(message = "名称不能为空", groups = {Add.class, Update.class})
    @Size(max = 20, message = "名称最大长度为20个字符", groups = {Add.class, Update.class})
    private String name;
    
    //  
    @NotNull(groups = {Add.class, Update.class})
    @Valid
    private List<Job> job;
}

@Data
public static class Job {
    @Min(value = 1, groups = Update.class)
    private Long jobId;

    @NotNull(groups = {Add.class, Update.class})
    @Length(min = 2, max = 10, groups = {Add.class, Update.class})
    private String jobName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

嵌套校验可以结合分组校验一起使用。

还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List<Job>字段会对这个list里面的每一个Job对象都进行校验。

# 2.6 集合校验

如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。

此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!

我们可以使用自定义list集合来接收参数,如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。

自定义List集合

public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

包装List类型,并声明@Valid注解。

@Delegate注解受lombok版本限制,1.18.6以上版本可支持。

Controller层的方法

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated ValidationList<User> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}
1
2
3
4
5

# 2.7 统一异常拦截处理

默认情况下spring对不符合validation验证的请求的处理是返回一个http 400错误码。

可以通过spring的ExceptionAdvice功能对异常进行统一的捕获和处理,校验失败返回的异常是BindException

@RestControllerAdvice
public class BusinessExceptionHandler {
	
    /**
     * 处理参数校验异常(requestBody参数校验)
     * `POST`、`PUT`请求一般会使用`requestBody`传递参数,如果校验失败,会抛出`MethodArgumentNotValidException`异常,`Spring`默认会将其转为400(Bad Request)请求。
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        logger.warn("【全局业务异常】\r\n参数校验异常记录:", e);
        Map<String, Object> error = new HashMap<>(2);
        if (e.getBindingResult().getFieldError() == null) {
            error.put("code", ResponseCode.PARAM_ERROR.code());
            error.put("message", ResponseCode.PARAM_ERROR.message());
        }
        MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
        BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
        String msg = bindingResult.getFieldErrors()
                .stream()
                .map(FieldError::getDefaultMessage)
                .distinct()
                .collect(Collectors.joining(","));
        error.put("code", HttpStatus.BAD_REQUEST.value());
        error.put("message", msg);
        return new ResponseEntity<>(error, HttpStatus.OK);
    }

    /**
     * 处理请求参数校验异常(requestParam/PathVariable参数校验)
     * GET请求一般会使用requestParam/PathVariable传参。如果校验失败,会抛出ConstraintViolationException异常。
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> constraintViolationHandler(ConstraintViolationException exception) {
        logger.error("【全局业务异常】\r\n参数校验异常记录:", exception);
        Map<String, Object> error = new HashMap<>(2);
        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
        if (violations.size() > 0) {
            List<String> list = new ArrayList<>();
            for (ConstraintViolation<?> violation : violations) {
                list.add(violation.getMessage());
            }
        }
        return new ResponseEntity<>(error, HttpStatus.OK);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

# 2.8 自定义validation

javax.validation中的注解能够满足大部分的校验需求,但是如果有一些特殊的校验,我们也可以进行自定义校验注解和规则。

步骤

  1. 先定义注解;
  2. 然后定义注解对应的ConstraintValidator。

示例:实现一个校验数字必须是奇数的校验。

  1. 首先定义一个注解,命名为OddNum;
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OddNumConstraintValidator.class)
public @interface OddNum {
    // 错误消息
    String message() default "必须是奇数";
    // 分组
    Class<?>[] groups() default { };
    // 负载
    Class<? extends Payload>[] payload() default { };
}
1
2
3
4
5
6
7
8
9
10
11

其中@Constraint的validatedBy用于指定对应的ConstraintValidator——即注解校验规则实现。

message() 、groups() 、payload() 几个方法要默认加上。

  1. 然后定义对应的OddNumConstraintValidator,这个类要实现ConstraintValidator,然后用泛型引用对应的注解和支持的校验对象类型。

我们这里奇数可以支持数字类型,使用Number类。

然后会需要实现两个方法:

  • 一个是initialize方法可以用来从注解中获取用户配置的值,比如@Max中的value值,保存在字段中在isValid方法中使用。

  • 然后实现isValid方法负责校验,value是对应的要校验的值。

public class OddNumConstraintValidator implements ConstraintValidator<OddNum, Number> {
    @Override
    public void initialize(OddNum constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Number value, ConstraintValidatorContext context) {
        return value != null && value.longValue() % 2 == 1;
    }
}
1
2
3
4
5
6
7
8
9
10
11

定义完成后,就可以和javax.validation包中的注解一样使用了。

@Data
public class User {
    @OddNum(message = "年龄必须是奇数", groups = {Add.class, Update.class})
    private Integer age;
}
1
2
3
4
5

# 2.9 编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。

这个时候可以注入javax.validation.Validator对象,然后再调用其api。

@Autowired
private javax.validation.Validator globalValidator;

// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody User user) {
    Set<ConstraintViolation<User>> validate = globalValidator.validate(user, Add.class);
    // 如果校验通过,validate为空;否则,validate包含未校验通过项
    if (validate.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.10 快速失败(Fail Fast)

Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}
1
2
3
4
5
6
7
8
9

# 2.11 Validator实现原理

使用spring validation(底层hibernate-validator)实现原理,以自定义校验注解@OddNum为例追踪:

  • 在spring mvc中,有ModelAttributeMethodProcessor类负责将请求参数绑定到RequestMapping方法的参数对象上,同时会进行参数校验。
  • 在validateIfApplicable中会调用binder.validate,spring会找到加载的validatator来进行validate。
  • 在classloader中具有hibernate-validator的情况下,会委托给hibernate的ValidatorImpl进行校验。 ValidatorImpl中,先从对应的类中读取相关的校验规则注解信息构成BeanMetaData,构建上下文信息BaseBeanValidationContext,然后调用validateInContext。
  • 最终会加载各个Constraint校验实现逻辑对目标bean进行校验,比如我们刚定义的OddNumConstraintValidator会在SimpleConstraintTree.validateConstraints中进行加载调用。 validateConstraints首先根据validationContext和valueContext获取对应的validator,在处理我们的@OddNum注解的参数时会获取到OddNumConstraintValidator。
  • 最后validateSingleConstraint方法,会调用OddNumConstraintValidator的isValid方法。

# 1. RequestBody参数校验实现原理

spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody标注的参数以及处理@ResponseBody标注方法的返回值的。

显然,执行参数校验的逻辑肯定就在解析参数的方法resolveArgument()中:

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

        parameter = parameter.nestedIfOptional();
        //将请求数据封装到DTO对象中
        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);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以看到,resolveArgument()调用了validateIfApplicable()进行参数校验。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。
        //如果没有,那么判断参数前是否有Valid起头的注解。
        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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

resolveArgument()调用了validateIfApplicable()进行参数校验。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。如果没有,那么判断参数前是否有Valid起头的注解。
        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;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

看到这里,大家应该能明白为什么这种场景下@Validated@Valid两个注解可以混用。

我们接下来继续看WebDataBinder.validate()实现。

# 2. 方法级别的参数校验实现原理

上面提到的将参数一个个平铺到方法参数中,然后在每个参数前面声明约束注解的校验方式,就是方法级别的参数校验。

实际上,这种方式可用于任何Spring Bean的方法上,比如Controller/Service等。其底层实现原理就是AOP,具体来说是通过MethodValidationPostProcessor动态注册AOP切面,然后使用MethodValidationInterceptor对切点方法织入增强。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

接着看一下MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

实际上,不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。

# 三、相关资料