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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 【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 开头的注解。
目录
相关文章
|
4天前
|
前端开发 Java Spring
Spring MVC 请求处理流程
Spring MVC 请求处理流程
6 0
|
8天前
|
JSON 前端开发 Java
Spring MVC 级联对象参数校验
【6月更文挑战第6天】在 Spring MVC 的使用过程中,我们会发现很多非常符合直觉的功能特性,但往往我们会习惯这种「被照顾得很好」的开发方式,依靠直觉去判断很多功能特性的用法。
14 1
|
Java Spring 前端开发
spring 3.2 自定义参数绑定--日期格式转换器
springmvc配置文件 <!-- 代替org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping 和org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter -->
2398 0
|
1月前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
78 0
|
1月前
|
缓存 Java Maven
Spring Boot自动配置原理
Spring Boot自动配置原理
62 0
|
1月前
|
缓存 安全 Java
Spring Boot 面试题及答案整理,最新面试题
Spring Boot 面试题及答案整理,最新面试题
168 0
|
1月前
|
存储 JSON Java
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
66 2
|
1月前
|
前端开发 搜索推荐 Java
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
|
1月前
|
前端开发 Java 应用服务中间件
Springboot对MVC、tomcat扩展配置
Springboot对MVC、tomcat扩展配置
|
4天前
|
Java Linux Shell
docker 打包 springboot 项目快速入门
docker 打包 springboot 项目快速入门
13 0