或者是负数,入参验证不会检测出来。
为了能够进行嵌套验证,必须手动在 Item 实体的 props 字段上明确指出这个字段里面的实体也要进行验证。由于@Validated 不能用在成员属性(字段)上,但是@Valid 能加在成员属性(字段)上,而且@Valid 类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid 加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated 或@Valid 来进行嵌套验证。
我们修改 Item 类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
然后我们在 ItemController 的 addItem 函数上再使用@Validated 或者@Valid,就能对 Item 的入参进行嵌套验证。此时 Item 里面的 props 如果含有 Prop 的相应字段为空的情况,Spring Validation 框架就会检测出来,bindingResult 就会记录相应的错误。
Spring Validation 原理简析
现在我们来简单分析下 Spring 校验功能的原理。
方法级别的参数校验实现原理
所谓的方法级别的校验就是指将@NotNull 和@NotEmpty 这些约束直接加在方法的参数上的。
比如
@GetMapping("/getUser")
@ResponseBody
public R getUser(@NotNull(message = "userId 不能为空") Integer userId){
//
}
或者
@Validated
@Service
public class ValidatorService {
private static final Logger logger = LoggerFactory.getLogger(ValidatorService.class);
public String show(@NotNull(message = "不能为空") @Min(value = 18, message = "最小18") String age) {
logger.info("age = {}", age);
return age;
}
}
都属于方法级别的校验。这种方式可用于任何 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());
}
}
接着看一下 MethodValidationInterceptor:
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//无需增强的方法,直接跳过
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
//获取分组信息
Class[] groups = determineva lidationGroups(invocation);
Executableva lidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<constraintviolation> 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;
}
}
DTO 级别的校验
@PostMappin