Spring Web参数校验底层原理你掌握了吗

133 阅读3分钟

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;
	}

	
}