Spring Web 嵌套对象校验失效

问题复现

  • 当开发一个学籍管理系统时,我们会提供了一个 API 接口去添加学生的相关信息,学生中有个嵌套属性联系电话,其对象定义参考下面的代码:

    java 复制代码
    import lombok.Data;
    import javax.validation.constraints.Size;
    @Data
    public class Student {
        @Size(max = 10)
        private String name;
        private short age;
        private Phone phone;
    }
    
    @Data
    class Phone {
        @Size(max = 10)
        private String number;
    }
  • 这里我们也给 Phone 对象做了合法性要求(@Size(max = 10)),当我们使用下面的请求(请求 body 携带一个联系电话信息超过 10 位),测试校验会发现这个约束并不生效。

  • 定义完对象后,我们再定义一个 Controller 去使用它,使用方法如下:

    java 复制代码
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    @Slf4j
    @Validated
    public class StudentController {
        @RequestMapping(path = "students", method = RequestMethod.POST)
        public void addStudent(@Validated @RequestBody Student student){
            log.info("add new student: {}", student.toString());
            //省略业务代码
        };
    }
  • 我们提供了一个支持学生信息添加的接口。启动服务后,使用 IDEA 自带的 HTTP Client 工具来发送下面的请求以添加一个学生:

    json 复制代码
    POST http://localhost:8080/students
    Content-Type: application/json
    {
      "name": "xiaoming",
      "age": 10,
      "phone": {"number":"12306123061230612306"}
    }
  • 发现校验器并没有生效。

案例解析

  • 关于 student 本身的 Phone 类型成员是否校验是在校验过程中(即案例 1 中的代码行 binder.validate(validationHints))决定的。

  • 在校验执行时,首先会根据 Student 的类型定义找出所有的校验点,然后对 Student 对象实例执行校验,这个逻辑过程可以参考代码 ValidatorImpl#validate:

    java 复制代码
    @Override
    public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
       //省略部分非关键代码
       Class<T> rootBeanClass = (Class<T>) object.getClass();
       //获取校验对象类型的"信息"(包含"约束")
       BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
    
       if ( !rootBeanMetaData.hasConstraints() ) {
          return Collections.emptySet();
       }
    
       //省略部分非关键代码
       //执行校验
       return validateInContext( validationContext, valueContext, validationOrder );
    }
  • 这里语句"beanMetaDataManager.getBeanMetaData( rootBeanClass )"根据 Student 类型组装出 BeanMetaData,BeanMetaData 即包含了需要做的校验(即 Constraint)。

  • 在组装 BeanMetaData 过程中,会根据成员字段是否标记了 @Valid 来决定(记录)这个字段以后是否做级联校验,参考代码 AnnotationMetaDataProvider#getCascadingMetaData:

    java 复制代码
    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 )"决定了 CascadingMetaDataBuilder#cascading 是否为 true。如果是,则在后续做具体校验时,做级联校验,而级联校验的过程与宿主对象(即 Student)的校验过程大体相同,即先根据对象类型获取定义再来做校验。

  • 在当前案例代码中,phone 字段并没有被 @Valid 标记,所以关于这个字段信息的 cascading 属性肯定是 false,因此在校验 Student 时并不会级联校验它。

问题修正

  • 从源码级别了解了嵌套 Validation 失败的原因后,我们会发现,要让嵌套校验生效,解决的方法只有一种,就是加上 @Valid,修正代码如下:

    java 复制代码
    @Valid
    private Phone phone;
  • 当修正完问题后,我们会发现校验生效了。而如果此时去调试修正后的案例代码,会看到 phone 字段 MetaData 信息中的 cascading 确实为 true 了,参考下图:

相关推荐
小江的记录本2 分钟前
【JVM虚拟机】类加载机制:类加载器、双亲委派模型、好处、破坏双亲委派的场景(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
Devin~Y22 分钟前
智慧物流+AIGC客服Java大厂面试:Spring Boot、Kafka、Redis、JVM与RAG Agent实战
java·jvm·spring boot·redis·spring cloud·kafka·rag
闪电悠米32 分钟前
黑马点评-分布式锁-02_simple_redis_lock_setnx
java·数据库·spring boot·redis·分布式·缓存·wpf
李白的天不白1 小时前
spring boot + vue3项目部署须知
java·spring boot·后端
小马爱打代码1 小时前
SpringBoot + Redis Stream + 消费组:替代 Kafka 轻量级消息队列,低延迟高吞吐
spring boot·redis
杨运交2 小时前
[026][数据模块]基于 MyBatis Plus 的企业级数据访问框架设计与实现
spring boot
小马爱打代码2 小时前
SpringBoot + 本地缓存 + 布隆过滤器:防止恶意 ID 查询打穿数据库
数据库·spring boot·缓存
hai3152475432 小时前
FiveOS V3.0 交付(微服务器操作系统版 · 物理合规修正
linux·人工智能·spring boot·后端·神经网络·机器学习
源码宝2 小时前
基于SpringBoot+Vue+小程序+Android的智慧校园电子班牌系统源码示例
vue.js·spring boot·架构·智慧校园·电子班牌·源码·代码
段ヤシ.3 小时前
【Java框架】知识点汇总Day7:Spring Boot +Vue(持续更新)
vue.js·spring boot·后端·框架