总得来说,我个人建议不能光只记忆结论,因为那很容易忘记,所以还是得稍微深入一点,让记忆更深刻吧。那就从下面四个方面深入:
检索Field:getFieldMetaData( beanClass )
- 拿到本类所有字段Field:clazz.getDeclaredFields()
- 把每个Field都包装成ConstrainedElement存放起来~~~1. 注意:此步骤完成了对每个Field上标注的注解进行了保存
检索Method:getMethodMetaData( beanClass )
- 拿到本类所有的方法Method:clazz.getDeclaredMethods()
- 排除掉静态方法和合成(isSynthetic)方法
- 把每个Method都转换成一个ConstrainedExecutable装着~~(ConstrainedExecutable也是个ConstrainedElement)。在此期间它完成了如下事(方法和构造器都复杂点,因为包含入参和返回值):1. 找到方法上所有的注解保存起来2. 处理入参、返回值(包括自动判断是作用在入参还是返回值上)
检索Constructor:getConstructorMetaData( beanClass )
完全同处理Method,略
检索Type:getClassLevelConstraints( beanClass )
- 找打标注在此类上的所有的注解,转换成ConstraintDescriptor
- 对已经找到每个ConstraintDescriptor进行处理,最终都转换Set<MetaConstraint<?>>这个类型
- 把Set<MetaConstraint<?>>用一个ConstrainedType包装起来(ConstrainedType是个ConstrainedElement)
关于级联校验此处补充说明一点,处理Type,都会处理级联校验情况,并且还是递归处理:
也就是这个方法(课件@Valid在此处生效):
// type解释:分如下N中情况 // Field为:.getGenericType() // 字段的类型 // Method为:.getGenericReturnType() // 返回值类型 // Constructor:.getDeclaringClass() // 构造器所在类 // annotatedElement:可不一定说一定要有注解才能进来(每个字段、方法、构造器等都能传进来) private CascadingMetaDataBuilder getCascadingMetaData(Type type, AnnotatedElement annotatedElement, Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) { return CascadingMetaDataBuilder.annotatedObject( type, annotatedElement.isAnnotationPresent( Valid.class ), containerElementTypesCascadingMetaData, getGroupConversions( annotatedElement ) ); }
这里对我们理解级联校验最重要的一句是:annotatedElement.isAnnotationPresent(Valid.class)。也就是说:若元素被此注解标注了,那就证明需要对它进行级联校验,这就是JSR定位@Valid的作用~
Spring提升了它???请关注后文Spring对它的应用吧~
ConstraintValidator.isValid()调用处
我们知道,每个约束注解都是交给约束校验器ConstraintValidator.isValid()这个方法来处理的,它被调用(生效)的地方在此(唯一处):
public abstract class ConstraintTree<A extends Annotation> { ... protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext, ValueContext<?, ?> valueContext, ConstraintValidatorContextImpl constraintValidatorContext, ConstraintValidator<A, V> validator) { ... V validatedValue = (V) valueContext.getCurrentValidatedValue(); isValid = validator.isValid( validatedValue, constraintValidatorContext ); ... // 显然校验不通过就返回错误消息 否则返回空集合 if ( !isValid ) { return executionContext.createConstraintViolations(valueContext, constraintValidatorContext); } return Collections.emptySet(); } ... }
这个方法的调用,会在执行每个Group
的时候
success = metaConstraint.validateConstraint( validationContext, valueContext );
MetaConstraint在上面检索的时候就已经准备好了,最后通过ConstrainedElement.getConstraints就拿到了每个元素的校验器们,继续调用
// ConstraintTree<A> boolean validationResult = constraintTree.validateConstraints( executionContext, valueContext );
so,最终就调用到了isValid这个真正做事的方法上了。
说了这么多,你可能还云里雾里,那么就show一把吧:
Demo Show
上面用一个示例校验Person这个JavaBean了,但是你会发现示例中我们全都是校验的Field属性。从理论里我们知道了Bean Validation它是有校验方法、构造器、入参甚至递归校验级联属性的能力的:
校验属性Field
略
校验Method入参、返回值
校验Constructor入参、返回值
既校验入参,同时也校验返回值
这些是不能直接使用的,需要在运行时进行校验。具体使用可参考:【小家Spring】让Controller支持对平铺参数执行数据校验(默认Spring MVC使用@Valid只能对JavaBean进行校验)
级联校验
什么叫级联校验,其实就是带校验的成员里存在级联对象时,也要对它完成校验。这个在实际应用场景中是比较常见的,比如入参Person
对象中,还持有Child
对象,我们不仅仅要完成Person
的校验,也依旧还要对Child内的属性校验:
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid @NotNull private InnerChild child; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } }
校验逻辑如下:
public static void main(String[] args) { Person person = new Person(); person.setName("fsx"); Person.InnerChild child = new Person.InnerChild(); child.setName("fsx-son"); child.setAge(-1); person.setChild(child); // 放进去 Validator validator = Validation.byProvider(HibernateValidator.class).configure().failFast(false) .buildValidatorFactory().getValidator(); Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出错误消息 result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()) .forEach(System.out::println); }
运行:
child.age 必须是正数: -1 age 不能为null: null
对child.age
这个级联属性校验成功~