Spring MVC 是如何对对象参数进行校验的

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
简介: 【6月更文挑战第4天】对象参数校验是使用 SpringMVC 时常用的功能,这篇文章尝试分析了,Spring 是如何实现这一功能的。

通过 HTTP 请求,向一个基于 SpringMVC 的后端提交一个实体信息的时候,我们通常会这样做:

在请求时,将要提交的实体信息放在请求的 Body 中。然后,在后端的 Controller 方法中,提供一个对应的实体类型的参数,SpringMVC 会自动将提交的信息转换成指定类型的对象,并传入方法的参数中,类似如下的代码:

@PostMapping("/users")
public Object user(@RequestBody User user) {
   
    /* 方法执行逻辑 */
}

大部分情况下,我们还会使用一些注解,来让 Spring 帮助我们校验参数,并在校验失败的时候,返回特定的提示信息。比如:

@Data
public class User{
   

    @Size(min = 6, max = 20)
    private String username;

}

此时,如果提交的用户名信息不再规定长度内,会自动响应错误提示信息。要使这一功能生效,还需要在 Controller 的方法参数前,添加 @Validated 注解,像这样:

@Validated @ResponseBody User user

这里的验证,是如何实现的呢?

我们都知道,SpringMVC 接收到的所有请求,都是由 DispatcherServlet 处理的,doDispatch 方法会根据请求的路径等信息,找到匹配的 Controller 方法,通过反射机制去调用匹配到的方法。

在调用方法之前,Spring 就需要创建方法中的各个参数。Spring 提供了很多 HandlerMethodArgumentResolver 来构建方法参数,在试图构建每一个参数的时候,Spring 会循环所有的 HandlerMethodArgumentResolver 通过调用其 supportsParameter 方法找到匹配的那一个,然后通过调用 resolveArgument 进行参数的构建(可以参考另一篇文章 自定义 Spring MVC Controller 方法参数处理)。

我们可以查看相应的源码。

查找参数处理器的源码,可以在 HandlerMethodArgumentResolverComposite 类的 getArgumentResolver方法中找到:

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
   
   HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
   if (result == null) {
   
      for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
   
         if (resolver.supportsParameter(parameter)) {
   
            result = resolver;
            this.argumentResolverCache.put(parameter, result);
            break;
         }
      }
   }
   return result;
}

执行参数处理的源码,可以在 HandlerMethodArgumentResolver 类的 resolveArgument 方法中找到:

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
   

   HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
   if (resolver == null) {
   
      throw new IllegalArgumentException("Unsupported parameter type [" +
            parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
   }
   return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

前面的例子中,我们的参数 user 被 @RequestBody 注解修饰,因此会匹配到 RequestResponseBodyMethodProcessor 类来处理,具体的处理源码如下:

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
   

   parameter = parameter.nestedIfOptional();
   Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
   String name = Conventions.getVariableNameForParameter(parameter);

   if (binderFactory != null) {
   
      WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
      if (arg != null) {
   
         validateIfApplicable(binder, parameter);
         if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
   
            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
         }
      }
      if (mavContainer != null) {
   
         mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
      }
   }

   return adaptArgumentIfNecessary(arg, parameter);
}

这里有一行代码 validateIfApplicable(binder, parameter); 就是用来执行参数校验的,当校验结果中包含错误信息的时候,会抛出参数校验错误的一场。这个方法的实现在它的父类 AbstractMessageConverterMethodProcessor 中,源码如下:

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
   Annotation[] annotations = parameter.getParameterAnnotations();
   for (Annotation ann : annotations) {
      Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
      if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
         Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
         Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
         binder.validate(validationHints);
         break;
      }
   }
}

在这个方法中,首先会获取到参数的所有注解进行遍历,当获取到 @Validated 注解,或者注解的类型名称以 Valid 开头,都会对这个参数进行对象校验。因此,对于需要执行校验的参数对象,我们可以有三种方法告诉 Spring 对它进行校验:

  1. 使用 @Validated 注解
  2. 使用 javax.validation 包中的 @Valid 的注解
  3. 使用一个自定义的名称以 Valid 开头的注解。
目录
相关文章
|
17天前
|
JSON 前端开发 Java
spring mvc Rest风格
spring mvc Rest风格
16 0
|
2天前
|
前端开发 Java 应用服务中间件
我以为我对Spring MVC很了解,直到我遇到了...
所有人都知道Spring MVC是是开发的,却鲜有人知道Spring MVC的理论基础来自于1978 年提出MVC模式的一个老头子,他就是Trygve Mikkjel Heyerdahl Reenskaug,挪威计算机科学家,名誉教授。Trygve Reenskaug的MVC架构思想早期用于图形用户界面(GUI) 的软件设计,他对MVC是这样解释的。MVC 被认为是解决用户控制大型复杂数据集问题的通用解决方案。最困难的部分是为不同的架构组件想出好的名字。模型-视图-编辑器是第一个。
我以为我对Spring MVC很了解,直到我遇到了...
|
11天前
|
前端开发 Java Spring
Spring MVC中使用ModelAndView传递数据
Spring MVC中使用ModelAndView传递数据
|
4天前
|
XML 前端开发 Java
Spring Boot与Spring MVC的区别和联系
Spring Boot与Spring MVC的区别和联系
|
7天前
|
Java 微服务 Spring
微服务04---服务远程调用,根据订单id查询订单功能,根据id查询订单的同时,把订单所属的用户信息一起返回,Spring提供了一个工具RestTemplate,Bean写在对象前面,以后可以在任何地
微服务04---服务远程调用,根据订单id查询订单功能,根据id查询订单的同时,把订单所属的用户信息一起返回,Spring提供了一个工具RestTemplate,Bean写在对象前面,以后可以在任何地
序-Servlet和SpringMVC的联系和区别-配置路径先想好使用的使用的方法,然后匹配的需要的技术
序-Servlet和SpringMVC的联系和区别-配置路径先想好使用的使用的方法,然后匹配的需要的技术
|
15天前
|
JSON 前端开发 数据格式
SpringMVC的数据响应-直接回写json字符串
SpringMVC的数据响应-直接回写json字符串
|
17天前
|
JSON 前端开发 Java
spring mvc 请求与响应
spring mvc 请求与响应
10 0
|
2月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
111 0