在开始本篇分析之前,先来看两篇文章:
1,基础理论知识:求求你别在用 IF ELSE 校验参数了
2,演示示例:SpringMVC/Boot中的校验框架 @Valid 和 @Validated的使用
这里注意:springmvc中只是引用了校验框架,真正的校验功能,springmvc没有实现。真正干事的是校验框架,所以本篇是分析,springmvc是如何使用校验框架的。
在执行处理器方法之前,一定是要解析出方法的参数值。不同的参数由不同的参数解析器负责。什么样的才会用到校验呢?答案:javaBean类型的;涉及到了解析复杂参数的ModelAttributeMethodProcessor,和用来处理@requestBody的RequestResponseBodyMethodProcessor。
RequestResponseBodyMethodProcessor
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);
// 如果校验有错误,并且这个参数的后面没有BindingResult类型的,那么就抛异常。
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 把这个绑定结果方法哦model中,处理器BindingResult类型的参数解析器得到值。
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
校验解析出来的参数,如果这个参数后面跟的是BindingResult,那么特定的参数解析器会将绑定结果解出来;如果没有跟,那么如果校验出错误,抛异常MethodArgumentNotValidException。
这一步的作用就是将绑定结果放到model中,等待ErrorsMethodArgumentResolver处理器解析出参数。
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
// org.springframework.web.method.annotation.ErrorsMethodArgumentResolver#resolveArgument
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null,
"Errors/BindingResult argument only supported on regular handler methods");
ModelMap model = mavContainer.getModel();
String lastKey = CollectionUtils.lastElement(model.keySet());
if (lastKey != null && lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
return model.get(lastKey);
}
throw new IllegalStateException(
"An Errors/BindingResult argument is expected to be declared immediately after " +
"the model attribute, the @RequestBody or the @RequestPart arguments " +
"to which they apply: " + parameter.getMethod());
}
下面看下是如何校验的;
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
// 遍历这个参数的注解。
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 如果有Validated注解,或者注解的名称是Valid开头,说明需要校验。
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;
}
}
}
public void validate(Object... validationHints) {
// 得到目标值
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// 得到所有的校验工具。进行校验。结果是放在bindingResult中。
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
ModelAttributeMethodProcessor#resolveArgument
。。。。
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
// 校验返回值
validateIfApplicable(binder, parameter);
// 这里抛异常是BindException
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
到目前为止校验的都是javaBean类型的。处理javabean,参数还有很多类型呀:integer类型,我想控制这个值在在某个范围;String类型,我想校验值的长度,等等;但这些普通类型的参数解析器不会校验,直接返回值。spring提供了另一种方式解决校验方法的功能。使用代理的方式,在参数都解析好了之后,我先校验下参数,之后再执行处理器方法。springmvc是如何代理的呢?
对bean的代理,就是操作bean喽,那么一定是bean处理器。看下这个bean处理器:
可以看到是springaop的逻辑。AbstractBeanFactoryAwareAdvisingPostProcessor
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
看这个方法,定义了起点和增强处理方法。
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
切点很好理解,就是对类的过滤,根据提供的条件。之后再对类中的方法进行过滤;
增强方法是处理的逻辑方法。
切点:对那些类进行过滤呢?
public AnnotationMatchingPointcut(Class<? extends Annotation> classAnnotationType, boolean checkInherited) {
// 对类的过滤方法
this.classFilter = new AnnotationClassFilter(classAnnotationType, checkInherited);
// 对方法过滤的方法,可以看到,这个逻辑是对类的额所有方法都应用。
this.methodMatcher = MethodMatcher.TRUE;
}
// 看类的匹配方法,类的上面要有Validated注解。
public boolean matches(Class<?> clazz) {
return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(clazz, this.annotationType) :
clazz.isAnnotationPresent(this.annotationType));
}
ok,过滤的条件就是标注Validated的类的所有方法。目标找到了,找到后做什么事情呢?这是增强的逻辑了。
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
// 切面,createMethodValidationAdvice返回的是增强类
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
// MethodValidationInterceptor是增强类
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
public Object invoke(MethodInvocation invocation) throws Throwable {
// FactoryBean.getObject类型的放不用代理
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
// 开始校验参数
try {
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// 如果有异常,校验这个方法对应的桥接方法
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
// 如果结果不是空,说明有问题,直接抛异常;
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// 没有异常,执行处理器方法,也就是Controller的方法
Object returnValue = invocation.proceed();
// 校验返回值。
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
可以看到使用校验框架进行校验。方法的参数校验,返回值也要校验。如果有错误。抛异常:ConstraintViolationException。
在强调一遍,springmvc的校验是基础校验组件的。自己并没有实现;所以可以用不同的校验组件,我们属性的就是Hibernate Validator ,如果想换校验组件;注入bean就可以。
还有最上面的文章标题是springmvc,springboot,这明显是错误的,springboot前端用的就是springmvc,他自己没有校验。springboot用的东西都用spring的,他只是做了自动装配。