validation-api与hibernate-validator;@Validated与@Valid

重写校验类Validator,可自定义获取校验信息:

@Component
public class ValidatorUtil implements ApplicationContextAware {

    // 通过Spring获得校验器
    private static Validator validator;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        Validator validatorBean = applicationContext.getBean(Validator.class);
        setValidator(validatorBean);
    }

    public static void setValidator(Validator validatorBean) {
        if (validatorBean instanceof LocalValidatorFactoryBean) {
            validator = ((LocalValidatorFactoryBean) validatorBean).getValidator();
        } else if (validatorBean instanceof SpringValidatorAdapter) {
            validator = validatorBean.unwrap(Validator.class);
        } else {
            validator = validatorBean;
        }
    }

    /**
     * 对配置了注解的对象进行校验
     */
    public static  void validata(T object) {
        Set> violationSet = validator.validate(object);
        for (ConstraintViolation violation : violationSet) {
            // 快速返回第一个校验失败的数据
            throw new ValidationException(violation.getMessage());
        }
    }

}

validation-api是一套标准,hibernate-validator实现了此标准

JSR-303 是Java EE 6 中的一项子规范,叫做BeanValidation,官方参考实现是hibernate-validator。

hibernate-validator实现了JSR-303规范

@Validated org.springframework.validation.annotation.Validated jar包:spring-context
@Valid javax.validation.Valid jar包:javax.validation

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。其中对于字段的特定验证注解比如@NotNull等网上到处都有,这里不详述

在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

  1. 分组

@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。

  1. 注解地方

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。

  1. 嵌套验证

@Valid可以加在子类上,进行嵌套验证

=================

1. 概述

  • @Valid是使用Hibernate validation的时候使用
  • @Validated是只用Spring Validator校验机制使用@Validation@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明

2. 注解位置

  • @Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
  • @Valid:可以用在方法、构造函数、方法参数和成员属性(field)上

如:

validation-api与hibernate-validator;@Validated与@Valid_第1张图片

3. 分组校验

  • @Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制
  • @Valid:没有分组功能

举例:定义分组接口

public interface IGroupA {
}

public interface IGroupB {
}

定义校验参数Bean

public class StudentBean implements Serializable {

   @NotBlank(message = "用户名不能为空")
   private String name;
   //只在分组为IGroupB的情况下进行验证
   @Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupB.class})
   private Integer age;
   @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
   private String phoneNum;
   @Email(message = "邮箱格式错误")
   private String email;
}

定义测试类:检验分组为IGroupA的情况

@RestController
public class CheckController {
    @PostMapping("stu")
    public String addStu(@Validated({IGroupA.class}) @RequestBody StudentBean studentBean){
        return "add student success";
    }
}

测试

validation-api与hibernate-validator;@Validated与@Valid_第2张图片

这里对分组IGroupB的就没检验了

如果改一下代码,改成下面这样,我们看一下测试结果

@RestController
public class CheckController {
   @PostMapping("stu")
   public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){
      return "add student success";
   }
}

测试结果:

validation-api与hibernate-validator;@Validated与@Valid_第3张图片

可以看到这种情况,就做了校验了

说明:1、不分 配groups,默认每次都要进行验证

2、对一个参数需要多种验证方式时,也可通过分配不同的组达到目的。

4. 组序列

默认情况下 不同级别的约束验证是无序的,但是在一些情况下,顺序验证却是很重要。一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。

举例:定义组序列

@GroupSequence({Default.class, IGroupA.class, IGroupB.class})
public interface IGroup {
}

需要校验的Bean,分别定义IGroupA对age进行校验,IGroupB对email进行校验:

public class StudentBean implements Serializable {

   @NotBlank(message = "用户名不能为空")
   private String name;

   //只在分组为IGroupB的情况下进行验证
   @Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupA.class})
   private Integer age;

   @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
   private String phoneNum;

   @Email(message = "邮箱格式错误", groups = {IGroupB.class})
   private String email;
}

测试代码:

@RestController
public class CheckController {
   @PostMapping("stu")
   public String addStu(@RequestBody @Validated({IGroup.class}) StudentBean studentBean){
      return "add student success";
   }
}

测试结果:

validation-api与hibernate-validator;@Validated与@Valid_第4张图片

可以看到只对IGroupA定义的错误进行了校验,IGroupB没有进行校验

嵌套校验

一个待验证的pojo类,其中还包含了待验证的对象,需要在待验证对象上注解@Valid,才能验证待验证对象中的成员属性,这里不能使用@Validated。

举例:需要约束校验的bean

public class TeacherBean {
   @NotEmpty(message = "老师姓名不能为空")
   private String teacherName;
   @Min(value = 1, message = "学科类型从1开始计算")
   private int type;
}

public class StudentBean implements Serializable {

   @NotBlank(message = "用户名不能为空")
   private String name;

   //只在分组为IGroupB的情况下进行验证
   @Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupA.class})
   private Integer age;

   @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
   private String phoneNum;

   @Email(message = "邮箱格式错误", groups = {IGroupB.class})
   private String email;

   @NotNull(message = "任课老师不能为空")
   @Size(min = 1, message = "至少有一个老师")
   private List teacherBeanList;
}

注意:

这里对teacherBeans只校验了NotNull, 和 Size,并没有对teacher信息里面的字段进行校验,具体测试如下:

validation-api与hibernate-validator;@Validated与@Valid_第5张图片

这里teacher中的type明显是不符合约束要求的,但是能检测通过,是因为在student中并没有做嵌套校验。

可以在teacherBeans中加上 @Valid,具体如下

@Valid
@NotNull(message = "任课老师不能为空")
@Size(min = 1, message = "至少有一个老师")
private List teacherBeanList;

测试结果如下:

validation-api与hibernate-validator;@Validated与@Valid_第6张图片

可以看到这种情况下就对TeacherBean中的参数进行了校验

======================

@Validate @Valid 工作原理

前言

SpringMvc中使用@Validate或者@Valid注解来校验Controller接口入参十分方便,那Spring中具体校验是如何生效呢?今天一起来探究下其工作原理。

对@RequestBody修饰的参数校验

对@RequestBody修饰的实体对象进行校验,是最常见的使用方式。SpringMvc参数解析是由HandlerMethodArgumentResolver的实现进行,具体解析逻辑位于#resolveArgument方法中。对于@RequestBody修饰的参数解析,准确的说由RequestResponseBodyMethodProcessor负责,通过观察其继承树可知,RequestResponseBodyMethodProcessor是HandlerMethodArgumentResolver接口的实现子类。

validation-api与hibernate-validator;@Validated与@Valid_第7张图片

RequestResponseBodyMethodProcessor#resolveArgument

@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) {
                        // 1
			validateIfApplicable(binder, parameter);
                        // 2
			if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
				throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
			}
		}
		if (mavContainer != null) {
                        //  3
			mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
		}
	}

	return adaptArgumentIfNecessary(arg, parameter);
}

RequestResponseBodyMethodProcessor#validateIfApplicable

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
	Annotation[] annotations = parameter.getParameterAnnotations();
	for (Annotation ann : annotations) {
                // 获取方法参数上的@Valid注解
		Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
                // 如果@Validate注解存在或者注解是以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;
		}
	}
}

MethodParameter代表的是方法参数的封装对象,首先获取方法参数上面所有的注解,遍历,判断注解是不是@Validate或者是否以Valid开头,如果是,则调用binder的#validate方法进行校验(binder对象中封装了具体的校验器validator),校验结果会封装到binder持有的BindingResult对象中。

RequestResponseBodyMethodProcessor#isBindExceptionRequired

protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
	int i = parameter.getParameterIndex();
	Class[] paramTypes = parameter.getExecutable().getParameterTypes();
	boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
	return !hasBindingResult;
}

如果BindingResult中有校验错误,需要调用该方法判断是否需要向外抛出MethodArgumentNotValidException异常。如果待校验方法除了@RequestBody修饰的参数之外还有其他参数,并且紧跟在@RequestBody修饰的参数之后的参数是Errors的子类,那么说明无需向外抛出异常,方法直接返回false,否则返回true。

mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());

将BindingResult存入ModelAndViewContainer对象中,以便后续处理Errors类型参数的解析。Errors类型参数交由ErrorsMethodArgumentResolver解析。

validation-api与hibernate-validator;@Validated与@Valid_第8张图片

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

从ModelAndViewContainer对象中取出BindingResult对象赋值给方法参数中的Errors对象,如果不满足条件会抛出IllegalStateException异常。

对@RequestBody修饰的参数校验小结

  • @RequestBody修饰的参数,添加@Validate或者@Valid注解都可以启用校验。
  • 如果@RequestBody修饰的参数后紧跟Errors类型参数,则校验错误信息最终会赋值给Errors类型参数,否则,会往外抛出MethodArgumentNotValidException异常。

@Validate修饰类进行方法参数校验

这种方式的原理与@RequestBody修饰参数的校验原理不同,不是通过HandlerMethodArgumentResolver参数解析器在解析方法参数的时候进行校验,而是通过Spring中大名鼎鼎的AOP创建动态代理对目标对象进行增强,在目标方法执行前进行方法参数校验。

SpringBoot项目启动时,如果依赖下有JSR-303规范的具体实现,容器中会自动注册MethodValidationPostProcessor这个Bean(具体注册细节,请参看ValidationAutoConfiguration,SpringBoot的自动配置原理这里不做过多说明)。

MethodValidationPostProcessor的继承树如下

validation-api与hibernate-validator;@Validated与@Valid_第9张图片

通过继承树可知,AbstractAdvisingBeanPostProcessor实现了BeanPostProcessor接口,具有了在Bean实例初始化前后回调增强的功能,同时又继承了ProxyProcessorSupport类,可以通过代理手段对Bean进行相应的增强。

MethodValidationPostProcessor并没有重写BeanPostProcessor的两个初始化回调方法,而是在AbstractAdvisingBeanPostProcessor中进行了重写。

AbstractAdvisingBeanPostProcessor#postProcessBeforeInitialization

public Object postProcessBeforeInitialization(Object bean, String beanName) {
	return bean;
}

发现在Bean的初始化前回调方法中什么都没做

AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization

public Object postProcessAfterInitialization(Object bean, String beanName) {
	if (this.advisor == null || bean instanceof AopInfrastructureBean) {
		// Ignore AOP infrastructure such as scoped proxies.
		return bean;
	}

	if (bean instanceof Advised) {
		Advised advised = (Advised) bean;
		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;
		}
	}

	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的初始化后回调方法中,如果满足条件,就会为Bean创建动态代理进行增强。那什么样的Bean会满足增强条件呢?AbstractAdvisingBeanPostProcessor#isEligible方法就是来判断某个Bean是否满足增强条件,虽然其子类AbstractBeanFactoryAwareAdvisingPostProcessor重写了该方法,但是只是先判断Bean是否是ORIGINAL_INSTANCE,不满足条件的情况下又会调用AbstractAdvisingBeanPostProcessor#isEligible进行判断。

AbstractAdvisingBeanPostProcessor#isEligible

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

该方法最终会调用AopUtils#canApply方法,判断传入的Advisor对象中的增强逻辑是否可作用于目标Bean。那么这个地方的Advisor对象具体是什么类型?该对象是在MethodValidationPostProcessor#afterPropertiesSet方法中创建,类型为DefaultPointcutAdvisor,构造方法中传入了AnnotationMatchingPointcut对象以及MethodValidationInterceptor对象。

AopUtils#canApply(Advisor advisor, Class targetClass, boolean hasIntroductions)

public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) {
	if (advisor instanceof IntroductionAdvisor) {
		return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
	}
	else if (advisor instanceof PointcutAdvisor) {
		PointcutAdvisor pca = (PointcutAdvisor) advisor;
		return canApply(pca.getPointcut(), targetClass, hasIntroductions);
	}
	else {
		// It doesn't have a pointcut so we assume it applies.
		return true;
	}
}

AopUtils#canApply(Pointcut pc, Class targetClass, boolean hasIntroductions)

public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) {
		Assert.notNull(pc, "Pointcut must not be null");
		if (!pc.getClassFilter().matches(targetClass)) {
			return false;
		}

		MethodMatcher methodMatcher = pc.getMethodMatcher();
		if (methodMatcher == MethodMatcher.TRUE) {
			// No need to iterate the methods if we're matching any method anyway...
			return true;
		}

		IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
		if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
			introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
		}

		Set> classes = new LinkedHashSet<>();
		if (!Proxy.isProxyClass(targetClass)) {
			classes.add(ClassUtils.getUserClass(targetClass));
		}
		classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

		for (Class clazz : classes) {
			Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
			for (Method method : methods) {
				if (introductionAwareMethodMatcher != null ?
						introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
						methodMatcher.matches(method, targetClass)) {
					return true;
				}
			}
		}

		return false;
	}

最终调用链走入上面的方法,传入的Pointcut对象就是AnnotationMatchingPointcut类型的对象,首先通过其持有的类过滤器进行过滤,如果无法匹配目标Class,就会返回false继而不会创建代理。AnnotationMatchingPointcut的逻辑通过名字就可以看出,判断目标Class是否被目标注解所修饰。通过AnnotationMatchingPointcut#afterPropertiesSet可以看到,创建的AnnotationMatchingPointcut对象持有的目标注解就是@Validated注解。

AnnotationMatchingPointcut#afterPropertiesSet

private Class validatedAnnotationType = Validated.class;
public void afterPropertiesSet() {
	Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
	this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}

所以,只要目标Bean所属Class类上被@Validate修饰,目标Class就会通过ClassFilter的校验,而AnnotationMatchingPointcut持有的MethodMatcher对象正是MethodMatcher.True,最终AopUtils#canApply方法会返回true。因此,只要目标Bean的类上被@Validate修饰,那么目标Bean就满足条件,会被创建代理对象织入相应的增强逻辑,即MethodValidationInterceptor中的增强逻辑。

代理对象的目标方法被调用之前,会被MethodValidationInterceptor#invoke方法拦截

MethodValidationInterceptor#invoke

public Object invoke(MethodInvocation invocation) throws Throwable {
	// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
	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> 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);
	}
	if (!result.isEmpty()) {
		throw new ConstraintViolationException(result);
	}

	Object returnValue = invocation.proceed();

	result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
	if (!result.isEmpty()) {
		throw new ConstraintViolationException(result);
	}

	return returnValue;
}

调用ExecutableValidator#validateParameters方法进行方法参数的校验,如果校验未通过,就会抛出ConstraintViolationException异常,如果校验通过,调用MethodInvocation#proceed继续调用目标方法,最后,调用ExecutableValidator#validateReturnValue方法进行方法返回值的校验,同理,校验未通过也会抛出ConstraintViolationException异常。

@Validate修饰类进行方法参数校验小结

  • 被校验参数的方法所在类一定要由@Validate注解修饰,只有这样,方法中被JSR-303规范中规定的注解修饰的参数才能被校验。
  • 参数校验不通过,会抛出ConstraintViolationException异常,应当在全局异常处理器中做统一处理。
  • 方法的入参和返回值均可以参与校验。

你可能感兴趣的:(spring,java,hibernate,java,后端)