通过 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 对它进行校验:
- 使用 @Validated 注解
- 使用 javax.validation 包中的 @Valid 的注解
- 使用一个自定义的名称以 Valid 开头的注解。