1、原始类型、原始类型包装类型 参数校验
@RestController
@Validated
public class AController(){
@GetMapping("/test")
public Result<Map<String, String>> test(@NotBlank(message = "请输入用户名") String name) {
}
}
参数校验实现原理:
基于Aop动态代理。切面实现:MethodValidationInterceptor
校验失败抛出异常类型:ConstraintViolationException
注意: 标注了上述校验注解的方法的目标类,必须标注@Validated注解。因为切入点表达式只会对标注@Validated的类机进行AOP动态代理
package org.springframework.validation.beanvalidation;
import java.lang.annotation.Annotation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.validation.annotation.Validated;
/**
* A convenient {@link BeanPostProcessor} implementation that delegates to a
* JSR-303 provider for performing method-level validation on annotated methods.
*
* <p>Applicable methods have JSR-303 constraint annotations on their parameters
* and/or on their return value (in the latter case specified at the method level,
* typically as inline annotation), e.g.:
*
* <pre class="code">
* public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
* </pre>
* 标注了上述校验注解的方法的目标类,必须标注@Validated注解
* <p>Target classes with such annotated methods need to be annotated with Spring's
* {@link Validated} annotation at the type level, for their methods to be searched for
* inline constraint annotations. Validation groups can be specified through {@code @Validated}
* as well. By default, JSR-303 will validate against its default group only.
*
* <p>As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider.
*
* @author Juergen Hoeller
* @since 3.1
* @see MethodValidationInterceptor
* @see javax.validation.executable.ExecutableValidator
*/
@SuppressWarnings("serial")
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
....
@Override
public void afterPropertiesSet() {
// 指定了切入点表达式,类必须标注@Validated注解
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
// 切面
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
2、非原始类型 参数校验
@RestController
public class AController{
@GetMapping(path = "/login")
public Result login(@Valid LoginVO loginVO) {
return Result.success("登录成功V1");
}
}
参数校验实现原理:
HandlerMethodArgumentResolver在解析方法参数时,会进行参数校验。
校验失败抛出异常类型: BindException
源码解析:
在RequestMappingHandlerAdapter中内置了许多参数解析器,未能够被定义在前面的解析器所解析的参数,最终都会被ServletModelAttributeMethodProcessor解析器进行解析(因为最后一个参数解析器是ServletModelAttributeMethodProcessor)。
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor 方法参数解析器进行解析,在其父类ModelAttributeMethodProcessor的resolveArgument方法中,在对LoginVo进行数据绑定的同时会进行参数校验。
ModelAttributeMethodProcessor#resolveArgument
3、@RequestBody 参数校验
@RestController
public class AController{
@PostMapping(path = "/login")
// 参数标注 @Valid和@Validated注解均可
public Result login(@RequestBody @Valid LoginVO loginVO) {
return Result.success("登录成功V1");
}
}
参数校验实现原理: HandlerMethodArgumentResolver在解析方法参数时,会进行参数校验。
校验失败抛出异常类型: MethodArgumentNotValidException
源码解析:
被@RequestBody标注的参数,会被RequestResponseBodyMethodProcessor进行参数解析,resolveArgument方法在使用消息转换器MessageConverters读取到参数值后,会进行参数校验。
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
....
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@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) {
// 如果参数被标注@Valid或者@Validated 则进行参数校验
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);
}
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
}