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 <T> void validata(T object) {
Set<ConstraintViolation<T>> violationSet = validator.validate(object);
for (ConstraintViolation<T> 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在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
-
分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
-
注解地方
@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。
-
嵌套验证
@Valid可以加在子类上,进行嵌套验证
=================
1. 概述
- @Valid是使用Hibernate validation的时候使用
- @Validated是只用Spring Validator校验机制使用@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明
2. 注解位置
- @Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
- @Valid:可以用在方法、构造函数、方法参数和成员属性(field)上
如:
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";
}
}
测试
这里对分组IGroupB的就没检验了
如果改一下代码,改成下面这样,我们看一下测试结果
@RestController
public class CheckController {
@PostMapping("stu")
public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){
return "add student success";
}
}
测试结果:
可以看到这种情况,就做了校验了
说明: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";
}
}
测试结果:
可以看到只对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<TeacherBean> teacherBeanList;
}
注意:
这里对teacherBeans只校验了NotNull, 和 Size,并没有对teacher信息里面的字段进行校验,具体测试如下:
这里teacher中的type明显是不符合约束要求的,但是能检测通过,是因为在student中并没有做嵌套校验。
可以在teacherBeans中加上 @Valid,具体如下
@Valid
@NotNull(message = "任课老师不能为空")
@Size(min = 1, message = "至少有一个老师")
private List<TeacherBean> teacherBeanList;
测试结果如下:
可以看到这种情况下就对TeacherBean中的参数进行了校验
======================
@Validate @Valid 工作原理
前言
SpringMvc中使用@Validate或者@Valid注解来校验Controller接口入参十分方便,那Spring中具体校验是如何生效呢?今天一起来探究下其工作原理。
对@RequestBody修饰的参数校验
对@RequestBody修饰的实体对象进行校验,是最常见的使用方式。SpringMvc参数解析是由HandlerMethodArgumentResolver的实现进行,具体解析逻辑位于#resolveArgument方法中。对于@RequestBody修饰的参数解析,准确的说由RequestResponseBodyMethodProcessor负责,通过观察其继承树可知,RequestResponseBodyMethodProcessor是HandlerMethodArgumentResolver接口的实现子类。
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解析。
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的继承树如下
通过继承树可知,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<Class<?>> 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<? extends Annotation> 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<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);
}
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异常,应当在全局异常处理器中做统一处理。
- 方法的入参和返回值均可以参与校验。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!