Spring MVC 级联对象参数校验

439 阅读2分钟

这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战

相关文章:Spring MVC 是如何对对象参数进行校验的

在上述的这篇文章里,介绍了我们常用的 Spring MVC 的对象参数校验,是怎么完成校验的过程的,这篇来介绍一个更深入的问题。因为上篇中介绍的过程,适用于对象中的属性都是常用的 Java 数据类型,比如基本数据类型或者字符串等,如果这个对象中包含一个其它类型的参数,会怎么样呢?

比如这样的类型:

@Data
public class User{

    @Size(min = 6, max = 20)
    private String username;
    
    private Avatar avatar;
    
    @Data
    public static class Avatar{
        
        @NotNull
        private String imageUrl;
        
    }

}

在 User 类型中,有一个 Avatar 类型的属性,Avatar 类型中有一个 String 类型的属性 imageUrl 需要校验非空。

在上一篇中,最后介绍了执行校验过程的 AbstractMessageConverterMethodProcessor 类中的 validateIfApplicable 方法,在这个方法中,最后对符合要求的参数对象执行校验的是:

binder.validate(validationHints);

其具体执行的代码,可以在 ValidatorImpl 类的 validate 方法中找到。

@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
   Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
   sanityCheckGroups( groups );

   @SuppressWarnings("unchecked")
   Class<T> rootBeanClass = (Class<T>) object.getClass();
   BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );

   if ( !rootBeanMetaData.hasConstraints() ) {
      return Collections.emptySet();
   }

   BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );

   ValidationOrder validationOrder = determineGroupValidationOrder( groups );
   BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
         validatorScopedContext.getParameterNameProvider(),
         object,
         validationContext.getRootBeanMetaData(),
         PathImpl.createRootPath()
   );

   return validateInContext( validationContext, valueContext, validationOrder );
}

在这里,会根据要校验的对象的类型,构建一个 BeanMetaData 类型的元信息,这个元信息中包含了需要做校验的字段和校验相关的信息。在构建 BeanMetaData 的过程中,会判断是否需要对成员字段进行级联校验,判断的方式就是这个字段是否被 @Valid 修饰(注意不是 @Validated)。级联校验的过程,与宿主对象的校验过程大体相同。

因此,文章开头的写法,Spring 不会去校验 Avatar 中的 imageUrl 是否合法,如果需要让这个校验生效,则需要给 avatar 属性增加 @Valid 注解:

@Valid
private Avatar avatar;

这样才会对 avatar 中的 imageUrl 进行校验。

在 Spring MVC 的使用过程中,我们会发现很多非常符合直觉的功能特性,比如将请求参数绑定到 Controller 的方法参数,或者直接返回一个 Java 对象就可以完成 JSON 数据的转换等等。但往往我们会习惯这种「被照顾得很好」的开发方式,依靠直觉去判断很多功能特性的用法。

如果遇到类似的问题,运行结果不符合开发时的预期,最好的方式就是搞懂其中的原理,从原理中寻找最佳地解决方式。