SpringMVC方法级别的参数验证

0 版本信息

Spring Boot:2.1.7.RELEASE

Spring: 5.1.9.RELEASE

validation-api: 2.0.1.Final

hibernate-validator: 6.0.17.Final

1 问题的提出

一般我们在写SpringMVC接口时,可能会使用@RequestParam来解释GET请求中的参数,比如我们想要根据userId获取用户详细信息:

@GetMapping("/user")
@ResponseBody
public Result<Map<String, Object>> test(@RequestParam String userId) {
    Map<String, Object> userDetail = new HashMap<String, Object>();
    userDetail.put("userId", userId);
    userDetail.put("userName", "test user");
    return new Result<Map<String, Object>>(userDetail);
}

上面例子存在的问题:

1. 只能保证userId在URL中是存在的,但不能保证userId是否有值

2. 不能限制userId的输入长度,假设userId的长度

2 方法级别的验证

要点:

1. 添加MethodValidationPostProcessor

2. 在需要进行方法验证的类添加@Validated注解

3. 给方法参数添加需要的验证注解

4. 需要捕获Controller中方法抛出的ConstraintViolationException

2.1 添加MethodValidationPostProcessor

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@SpringBootApplication
public class MethodLevelValidationApplication {

	public static void main(String[] args) {
		SpringApplication.run(MethodLevelValidationApplication.class, args);
	}
	
    // 要点1:新增MethodValidationPostProcessor这个Bean
	@Bean
	public MethodValidationPostProcessor methodValidationPostProcessor() {
		return new MethodValidationPostProcessor();
	}

}

2.2 给Controller中的方法添加方法级别的数据验证

package com.example.demo;

import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.NotBlank;

import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Validated // 要点2:在需要进行方法验证的类添加@Validated注解
public class TestController {
	
	@GetMapping("/user")
	@ResponseBody
	public Result<Map<String, Object>> test(
        // 要点3:给方法参数添加需要的验证注解
		@NotBlank(message = "userId must not be blank") 
		@Length(min = 6, max = 6, message = "the length of userId must be {min} ") 
		@RequestParam String userId) {
		return new Result.Builder<Map<String, Object>>()
			.data(getUserDetailById(userId))
			.build();
	}
	
	private Map<String, Object> getUserDetailById(String userId) {
		Map<String, Object> userDetail = new HashMap<String, Object>();
		userDetail.put("userId", userId);
		userDetail.put("userName", "test user");
		return userDetail;
	}
	
}

2.3 ControllerAdvice处理数据验证异常

package com.example.demo;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// 要点4:需要捕获Controller中方法抛出的ConstraintViolationException
@RestControllerAdvice
public class GlobalExceptionHander {

	@ExceptionHandler(value = RuntimeException.class)
	public String validationHander(RuntimeException e) {
		return e.toString();
	}
	
	@ExceptionHandler(value = ConstraintViolationException.class)
	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
	public Result<Void> validationHandler(ConstraintViolationException e) {
		StringBuilder message = new StringBuilder("Invalid Request Parameters: ");
		
		for(ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) {
			message.append(constraintViolation.getMessage())
				.append(", received value is '")
				.append(constraintViolation.getInvalidValue())
				.append("'; ");
		}
		return new Result.Builder<Void>().message(message.toString()).build();
	}
}

2.4 示例请求

RequestResponse
http://localhost:8080/user?userId=111111// 正常情况
{
    "data": {
        "userName": "test user", 
        "userId": "111111"
    }, 
    "extraData": null, 
    "message": null
}

http://localhost:8080

/user?userId=

// 异常情况
{
    "data": null, 
    "extraData": null, 
    "message": "Invalid Request Parameters: the length of userId must be 6 , received value is ''; userId must not be blank, received value is ''; "
}

3 原理解析

在前面,我们注册了一个MethodValidationPostProcessor这个Bean,那么这个Bean有什么用处呢?

3.1 MethodValidationPostProcessor

这个类是BeanPostProcessor接口(其可以在Bean初始化前或后做一些操作)的实现类,其内部持有一个JSR-303的provider,利用这个provider来执行方法级别的验证。

一般来说会使用行内验证注解,就像下面这样:

public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)

如果上面的方法想要执行验证,就需要在目标类上添加Spring的@Validated注解。当然也可以在@Validated注解上指定group,默认是使用默认group。

注意:从Spring5.0开始,这个功能需要Bean Validation 1.1 Provider。

其代码如下:

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
		implements InitializingBean {
	// 默认的验证注解是@Validated
	private Class<? extends Annotation> validatedAnnotationType = Validated.class;

	// 持有一个validator实例,用以进行验证
	@Nullable
	private Validator validator;

	// 验证注解也可以使用自定义的注解,并不是一定需要使用@Validated注解
	public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
		Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
		this.validatedAnnotationType = validatedAnnotationType;
	}

    // 设置JSR-303Validator,默认值是default ValidatorFactory中的default validator。
	public void setValidator(Validator validator) {
		// Unwrap to the native Validator with forExecutables support
		if (validator instanceof LocalValidatorFactoryBean) {
			this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
		}
		else if (validator instanceof SpringValidatorAdapter) {
			this.validator = validator.unwrap(Validator.class);
		}
		else {
			this.validator = validator;
		}
	}
	
    // 设置ValidatorFactory,本质上还是去获取validator
	public void setValidatorFactory(ValidatorFactory validatorFactory) {
		this.validator = validatorFactory.getValidator();
	}

	// 要点1:Bean属性设置完后设置切面和通知
	@Override
	public void afterPropertiesSet() {
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}

    // 创建AOP增强,用以进行方法验证。切点是指定@Validated注解的地方,增强是一个MethodValidationInterceptor。
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}

}

这里使用了Spring AOP实现了增强,那么这个advice究竟做了什么呢?

3.2 MethodValidationInterceptor

这个类实现了MethodInterceptor接口,其内部持有一个validator实例,使用这个validator实例实现验证。其代码如下:

public class MethodValidationInterceptor implements MethodInterceptor {
	// 持有validator实例,用以实现验证
	private final Validator validator;

    // 默认构造方法:使用默认的JSR-303 validator
	public MethodValidationInterceptor() {
		this(Validation.buildDefaultValidatorFactory());
	}
	// 构造方法:使用validatorFactory中的validator
	public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
		this(validatorFactory.getValidator());
	}
	// 构造方法:使用指定的validator
	public MethodValidationInterceptor(Validator validator) {
		this.validator = validator;
	}

	// 要点2:实现MethodInterceptor中的invoke方法
	@Override
	@SuppressWarnings("unchecked")
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}
		
        // 决定使用哪一个group进行验证
		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) {
			// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
			// Let's try to find the bridged method on the implementation class...
			methodToValidate = BridgeMethodResolver.findBridgedMethod(
					ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
			result = execVal.validateParameters(
					invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		}
        // 如果方法不满足验证,则抛出ConstraintViolationException异常
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}
		
        // 调用方法,并获取方法返回值
		Object returnValue = invocation.proceed();

        // 对方法返回值进行验证,如果不满足验证,抛出ConstraintViolationException异常
		result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}

    // 判断方法是不是FactoryBean中的getObject和isSingleton方法
	private boolean isFactoryBeanMetadataMethod(Method method) {
		Class<?> clazz = method.getDeclaringClass();

		// Call from interface-based proxy handle, allowing for an efficient check?
		if (clazz.isInterface()) {
			return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
					!method.getName().equals("getObject"));
		}

		// Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
		Class<?> factoryBeanType = null;
		if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
			factoryBeanType = SmartFactoryBean.class;
		}
		else if (FactoryBean.class.isAssignableFrom(clazz)) {
			factoryBeanType = FactoryBean.class;
		}
		return (factoryBeanType != null && !method.getName().equals("getObject") &&
				ClassUtils.hasMethod(factoryBeanType, method.getName(), method.getParameterTypes()));
	}

	// 决定使用哪一个validation group
	protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
		Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
		if (validatedAnn == null) {
			validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
		}
		return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
	}

}

这里我们已经看到JSR-303执行验证的代码,那么这个切面又是什么时候加进去的呢?

3.3 AbstractAdvisingBeanPostProcessor

看类名就知道这是个抽象类,并且实现了BeanPostProcessor接口。

其会在Bean初始化后,给特定的Bean应用Spring AOP。代码如下:

public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {
	// 持有一个Advisor实例
	@Nullable
	protected Advisor advisor;

    // 标记上面的advisor实例是否要放到其它advisor的前面
	protected boolean beforeExistingAdvisors = false;
	
    // 符合条件的Bean
	private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);

	public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) {
		this.beforeExistingAdvisors = beforeExistingAdvisors;
	}

	// 在bean初始化前不做任何事情
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}

    // 要点3:给bean添加advice
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if (this.advisor == null || bean instanceof AopInfrastructureBean) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}
		
        // 要点3-1:这个Bean不是原始Bean,已经添加过其它advice
		if (bean instanceof Advised) {
			Advised advised = (Advised) bean;
            // 如果这个Advised实例没有被冻结,并且这个Bean符合要求,则添加advice
			if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
				// Add our local Advisor to the existing proxy's Advisor chain...
				if (this.beforeExistingAdvisors) {
					advised.addAdvisor(0, this.advisor);
				}
				else {
					advised.addAdvisor(this.advisor);
				}
				return bean;
			}
		}
		
        // 要点3-2:如果这个bean满足条件,则创建一个ProxyFactory,返回CGLIB代理对象
		if (isEligible(bean, beanName)) {
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			if (!proxyFactory.isProxyTargetClass()) {
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
			proxyFactory.addAdvisor(this.advisor);
			customizeProxyFactory(proxyFactory);
			return proxyFactory.getProxy(getProxyClassLoader());
		}

		// No proxy needed.
		return bean;
	}

    // 判断bean是否能够应用当前的advisor
	protected boolean isEligible(Object bean, String beanName) {
		return isEligible(bean.getClass());
	}
	
    // 判断给定的类是否能够应用advisor
	protected boolean isEligible(Class<?> targetClass) {
		Boolean eligible = this.eligibleBeans.get(targetClass);
		if (eligible != null) {
			return eligible;
		}
		if (this.advisor == null) {
			return false;
		}
		eligible = AopUtils.canApply(this.advisor, targetClass);
		this.eligibleBeans.put(targetClass, eligible);
		return eligible;
	}
    // 为指定的bean准备ProcyFactory对象。子类可以在给ProxyFactory添加完advisor后对其进行修改。
	protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);
		proxyFactory.setTarget(bean);
		return proxyFactory;
	}

    // 子类可以选择实现这个方法来修改ProxyFactory
	protected void customizeProxyFactory(ProxyFactory proxyFactory) {
	}

}

参考

1. [苦B程序员的数据验证之路](http://www.mamicode.com/info-detail-323320.html)

2. [Java Bean Validation 最佳实践](https://www.cnblogs.com/beiyan/p/5946345.html)

3. [Spring Doc](https://docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#beans-factory-programmatically-registering-beanpostprocessors)

4. [Spring方法级别数据校验:@Validated + MethodValidationPostProcessor](https://segmentfault.com/a/1190000019891248?utm_source=tag-newest)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

94甘蓝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值