@Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】(中)

简介: @Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】(中)

RequestResponseBodyMethodProcessor


这个类应该是陌生的,在上面推荐的处理MVC返回值的文章中有提到过它:它能够处理@ResponseBody注解返回值(请参考它的supportsReturnType()方法~)

它还有另一个能力是:它能够处理请求参数(当然也是标注了@RequestBody它的~)

所以它既是个处理返回值的HandlerMethodReturnValueHandler,有是一个处理入参的HandlerMethodArgumentResolver。所以它命名为Processor而不是Resolver/Handler嘛,这就是命名的艺术~

// @since 3.1
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
  }
  // 类上或者方法上标注了@ResponseBody注解都行
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
  }
  // 这是处理入参封装校验的入口,也是本文关注的焦点
  @Override
  public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    // 它是支持`Optional`容器的
    parameter = parameter.nestedIfOptional();
    // 使用消息转换器HttpInputMessage把request请求转换出来,拿到值~~~
    // 此处注意:比如本例入参是Person类,所以经过这里处理会生成一个空的Person对象出来(反射)
    Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    // 获取到入参的名称,其实不叫形参名字,应该叫objectName给校验时用的
    // 请注意:这里的名称是类名首字母小写,并不是你方法里写的名字。比如本利若形参名写为personAAA,但是name的值还是person
    // 但是注意:`parameter.getParameterName()`的值可是personAAA
    String name = Conventions.getVariableNameForParameter(parameter);
    // 只有存在binderFactory才会去完成自动的绑定、校验~
    // 此处web环境为:ServletRequestDataBinderFactory
    if (binderFactory != null) {
      WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
      // 显然传了参数才需要去绑定校验嘛
      if (arg != null) {
        // 这里完成数据绑定+数据校验~~~~~(绑定的错误和校验的错误都会放进Errors里)
        // Applicable:适合
        validateIfApplicable(binder, parameter);
        // 若有错误消息hasErrors(),并且仅跟着的一个参数不是Errors类型,Spring MVC会主动给你抛出MethodArgumentNotValidException异常
        // 否则,调用者自行处理
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
          throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
        }
      }
      // 把错误消息放进去 证明已经校验出错误了~~~
      // 后续逻辑会判断MODEL_KEY_PREFIX这个key的~~~~
      if (mavContainer != null) {
        mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
      }
    }
    return adaptArgumentIfNecessary(arg, parameter);
  }
  // 校验,如果合适的话。使用WebDataBinder,失败信息最终也都是放在它身上~  本方法是本文关注的焦点
  // 入参:MethodParameter parameter
  protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 拿到标注在此参数上的所有注解们(比如此处有@Valid和@RequestBody两个注解)
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
      // 先看看有木有@Validated
      Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
      // 这个里的判断是关键:可以看到标注了@Validated注解 或者注解名是以Valid打头的 都会有效哦
      //注意:这里可没说必须是@Valid注解。实际上你自定义注解,名称只要一Valid开头都成~~~~~
      if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
        // 拿到分组group后,调用binder的validate()进行校验~~~~
        // 可以看到:拿到一个合适的注解后,立马就break了~~~
        // 所以若你两个主机都标注@Validated和@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. DataBinder使传入的objectName并不是形参名(形参名Spring是有这能力可以获取到的),而是类名首字母小写
  2. 关于此处给入参Person类赋值这块,和DataBinder的bind方法没有半毛钱关系~(都是MessageConverters去把值从Request里解析出来封装成对象的)

数据绑定的bind方法内建的只有一个地方用到了:ServletModelAttributeMethodProcessor。并且使用的还是子类ServletRequestDataBinder的bind(ServletRequest request)方法,父类的bind(PropertyValues pvs)内建都没有被调用过~~~


本文我们只着眼于关注@Valid的数据校验这块,有几个相关的使用小细节,总结如下:


  1. 形参(@RequestBody标注的入参)的name(形参名)和你写什么无关,若是实体类名字就是类名首字母小写。(若是数组、集合等,都会有自己特定的名称)
  2. @Validated和@Valid都能使校验生效,但却不仅仅是它哥俩才能行:任何名称是"Valid"打头的注解都能使得数据校验生效1. 自定义注解名称以Valid开头,并且给个value属性同样能够指定Group分组2. 个人直接建议使用@Validated即可,而去使用@Valid了,更不用自己给自己找麻烦去自定义注解啥的了~
  3. 只有当Errors(BindingResult)入参是是仅跟着@Valid注解的实体,Spring MVC才会把错误消息放权交给调用者处理,否则(没有或者不是紧挨着)它会抛出MethodArgumentNotValidException异常~


这是使用@RequestBody结合@Valid完成数据校验的基本原理。其实当Spring MVC在

处理@RequestPart注解入参数据时,也会执行绑定、校验的相关逻辑。对应处理器是RequestPartMethodArgumentResolver,原理大体上和这相似,它主要处理Multipart相关,本文忽略~



此处提示一个点,此文发出去后有一个好奇的小宝宝问我入参能使用多个对象并且都用@RequestBody标注吗?

关于这个问题姑且先不考虑合理与否,我们这样做试试:

    @PostMapping("/hello")
    public Object helloPost(@Valid @RequestBody Person personAAA, BindingResult result, @Valid @RequestBody Person personBBB) {
      ...
    }


请求却报错了:

Resolved [org.springframework.http.converter.HttpMessageNotReadableException: I/O err


错误消息很好理解:请求域的Body体是只能被读取一次的(流只能被读取一次嘛)。

若你是好奇的,你可能还会问:URL参数呢?请求链接?后面的参数呢,如何封装???因为本部分内容不是本文的关注点,若有兴趣请出门左拐~


说明:关于使用Map、List、数组等接受@RequestBody参数的情况类似,区别在于绑定器上,对Map、List的校验前面文章有过讲解,此处就不展开了。


希望读者能掌握这部分内容,因为它和面向使用者比较重要的@InitBinder强关联~~~


实际使用中一般使用@Validated分组校验(若需要),然后结合全局异常的处理方式来友好的对调用者展示错误消息~

全局异常处理示例


当校验失败时,Spring会抛出MethodArgumentNotValidException异常,该异常会持有校验结果对象BindingResult,从而获得校验失败信息。本处只给示例,仅供参考:

@RestControllerAdvice
public class MethodArgumentNotValidExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder stringBuilder = new StringBuilder();
        for (FieldError error : bindingResult.getFieldErrors()) {
            String field = error.getField();
            Object value = error.getRejectedValue();
            String msg = error.getDefaultMessage();
            String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
            stringBuilder.append(message).append("\r\n");
        }
        return Result.error(MsgDefinition.ILLEGAL_ARGUMENTS.codeOf(), stringBuilder.toString());
    }
}
相关文章
|
4月前
|
缓存 Java 开发者
【Spring】原理:Bean的作用域与生命周期
本文将围绕 Spring Bean 的作用域与生命周期展开深度剖析,系统梳理作用域的类型与应用场景、生命周期的关键阶段与扩展点,并结合实际案例揭示其底层实现原理,为开发者提供从理论到实践的完整指导。
589 22
|
4月前
|
人工智能 Java 开发者
【Spring】原理解析:Spring Boot 自动配置
Spring Boot通过“约定优于配置”的设计理念,自动检测项目依赖并根据这些依赖自动装配相应的Bean,从而解放开发者从繁琐的配置工作中解脱出来,专注于业务逻辑实现。
1670 0
|
3月前
|
XML Java 测试技术
《深入理解Spring》:IoC容器核心原理与实战
Spring IoC通过控制反转与依赖注入实现对象间的解耦,由容器统一管理Bean的生命周期与依赖关系。支持XML、注解和Java配置三种方式,结合作用域、条件化配置与循环依赖处理等机制,提升应用的可维护性与可测试性,是现代Java开发的核心基石。
|
3月前
|
XML Java 应用服务中间件
【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
SpringBoot专栏开篇第一章,讲述认识SpringBoot、Bean容器功能的讲解、自动装配原理的入门,还有其他常用的Springboot注解!如果想要了解SpringBoot,那么就进来看看吧!
494 3
|
5月前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
6月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
227 0
|
6月前
|
监控 架构师 NoSQL
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
Java Spring
Spring原理学习系列之一:注解原理解析
对于Spring注解大家肯定都不陌生,在日常开发工作中也会经常使用到注解。有时候提问小伙伴,注解的原理是什么,大部分都回答是利用了反射机制。但是继续深入提问,在Spring中是如何解析这些自带注解以及注解到底在什么时候起作用等问题时,很多人都会犯嘀咕。同样我在实际使用的过程中,也会有相同的困惑。所以一直想探究下注解实际的工作原理以及设计思想。用此文记录下自己对于注解原理的理解,也为有同样疑问的小伙伴提供些不同的理解角度。 原理解析 使用实例
Spring原理学习系列之一:注解原理解析
|
6月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1084 0
|
7月前
|
人工智能 Java 测试技术
Spring Boot 集成 JUnit 单元测试
本文介绍了在Spring Boot中使用JUnit 5进行单元测试的常用方法与技巧,包括添加依赖、编写测试类、使用@SpringBootTest参数、自动装配测试模块(如JSON、MVC、WebFlux、JDBC等),以及@MockBean和@SpyBean的应用。内容实用,适合Java开发者参考学习。
820 0