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 } |
// 異常情況 { "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)