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 示例請求

Request Response
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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章