Spring Boot下@Valid和@Validated的区别【源码级】

Spring Boot项目里对于接口参数校验,可以使用javax.validation.constraints包下的注解来优雅的校验。比如参数长度、是否为null甚至可以使用正则表达式来校验参数格式,以及校验不通过返回的提示信息都可以通过注解进行配置,实在是方便的很。

但是本人在开发中遇到了这么几个问题:

  • 某一次SpringBoot版本升级后,启动项目提示我没有·javax.validation·的依赖,一开始导入了javax.validation-validation-api仍旧不能启动,后来导入了hibernate-validator的依赖就可以了。为什么?
  • 有的文章说,使用这个校验器要在controller上加@Validated注解,同时接口参数里也要加才能使用,really?
  • @Valid是javax包下的注解,而@Validated是Spring的注解,在Spring Boot项目里得使用后者才行。确定?

基于以上几个问题,我翻阅了不少资料,debug好几轮源码才找到了答案,有兴趣得伙伴可以跟着我一起来看看。


Spring Boot 2.3

2.3版本的SpringBoot将不再依赖javax.validation的包,所以,开发人员需要自行导入依赖,官方推荐的是使用自家的这个依赖。

Validation Starter no longer included in web starters
As of #19550, Web and WebFlux starters do not depend on the validation starter by default anymore. If your application is using validation features,
for example you find that javax.validation.* imports are not being resolved, you’ll need to add the starter yourself.
For Maven builds, you can do that with the following:

<dependency>
  <groupId>org.springframework.bootgroupId>
  <artifactId>spring-boot-starter-validationartifactId>
dependency>

看下这个dependency的依赖关系
Spring Boot下@Valid和@Validated的区别【源码级】_第1张图片
它其实还是依赖了hibernate校验器的包,毕竟它是一个成熟的工具了,Spring当然是取其精华,不再自己搞一套了。
值得注意的是,这个jakarta.validation又是什么鬼?怎么和javax.validation不一样呢。打开这个包看看,其实是一样的。
Spring Boot下@Valid和@Validated的区别【源码级】_第2张图片

javax.validation-validation-api

这个包其实就是一个api的包,里边包含了所有的注解及接口,但是没有实现,所以若是只导入了这个依赖,是不行滴

        <dependency>
            <groupId>javax.validationgroupId>
            <artifactId>validation-apiartifactId>
            <version>2.0.1.Finalversion>
        dependency>

再往深想一步,为什么不行?

先看一下这个包里边最重要的一个类Validator,它定义了一些接口,供实现类去实现

public interface Validator {

	<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);

	<T> Set<ConstraintViolation<T>> validateProperty(T object,String propertyName,Class<?>... groups);

	<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
												  String propertyName,
												  Object value,
												  Class<?>... groups);

	BeanDescriptor getConstraintsForClass(Class<?> clazz);

	<T> T unwrap(Class<T> type);

	ExecutableValidator forExecutables();
}

接着参数校验肯定是在执行接口Handler前做掉的,也就是将参数封装成对象后,要进行参数校验,在这一步肯定会有类似校验器的东西(即Validator的实现类)去执行具体的参数校验

我们debug源码看看,源码只列出核心部分,多余内容都用省略号表示

Spring MVC的请求分发

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	...
	// Actually invoke the handler.
	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	...
}

一直往下走,走到参数校验的位置

@Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
       ...
        if (bindingResult == null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {
                if (!mavContainer.isBindingDisabled(name)) {
                    this.bindRequestParameters(binder, webRequest);
                }
				// 如果需要验证就进行参数校验 (就是看你参数上有没有使用了相关的注解)
                this.validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }
		...  
    }
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        // 获取参数注解
        Annotation[] var3 = parameter.getParameterAnnotations();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Annotation ann = var3[var5];
            // 根据注解找到匹配的参数
            Object[] validationHints = this.determineValidationHints(ann);
            if (validationHints != null) {
                binder.validate(validationHints);
                break;
            }
        }

    }

继续debug

    @Nullable
    private Object[] determineValidationHints(Annotation ann) {
        Validated validatedAnn = (Validated)AnnotationUtils.getAnnotation(ann, Validated.class);
        if (validatedAnn == null && !ann.annotationType().getSimpleName().startsWith("Valid")) {
            return null;
        } else {
            Object hints = validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann);
            if (hints == null) {
                return new Object[0];
            } else {
                return hints instanceof Object[] ? (Object[])((Object[])hints) : new Object[]{hints};
            }
        }
    }

这一步很关键,首先会判断方法参数里有没有使用@Validated注解,如果没有使用,就判断使用的注解是不是@Valid开头的注解,如果是就认为需要被校验,所以这两个注解都是可以使用的,并没有说Spring Boot项目必须要用Spring自己的注解

然后,跳回刚才那个方法,判断完该方法参数需要被校验后,就要开始执行校验

  protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] var3 = parameter.getParameterAnnotations();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Annotation ann = var3[var5];
            Object[] validationHints = this.determineValidationHints(ann);
            if (validationHints != null) {
            	// 参数校验
                binder.validate(validationHints);
                break;
            }
        }

    }
public void validate(Object... validationHints) {
		Object target = getTarget();
		Assert.state(target != null, "No target to validate");
		BindingResult bindingResult = getBindingResult();
		// Call each validator with the same binding result
		// 获取校验器,发现没有
		for (Validator validator : getValidators()) {
			if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
				((SmartValidator) validator).validate(target, bindingResult, validationHints);
			}
			else if (validator != null) {
				validator.validate(target, bindingResult);
			}
		}
	}

在for循环里,有个getValidators()的方法,跟进去发现,并没有校验器可以使用,后面就不用说了,自然就是不能够对参数进行校验了。
Spring Boot下@Valid和@Validated的区别【源码级】_第3张图片

看到这一步能得出两个结论

  • @Validated注解和@Valid都是可以被识别的,用哪一个都可以,并且只需要在方法参数上加注解就行,类上边不需要加注解。
  • javax.validation-validation-api这个依赖只提供校验器接口,没有具体的实现,所以不能完成参数校验,所以要完成校验工作还得引入hibernate-validator的依赖,这里是提供了校验器的实现,我截图给大家看一下
    Spring Boot下@Valid和@Validated的区别【源码级】_第4张图片
    这个校验器才是真正干活的。

我也把调用栈放上来,供大家参考
Spring Boot下@Valid和@Validated的区别【源码级】_第5张图片

hibernate的validator的加载

现在还剩下一个问题,这个hibernate校验器是什么时候被加载进来的呢?我也没做什么配置啊?

我们继续往下看

是否有校验器可用,关键看DataBinder类中的校验器list是否不为空
private final List validators = new ArrayList<>();

这个list是什么时候被初始化的呢

在开始参数校验之前有个创建DataBinder方法,我们一起看一下

    if (bindingResult == null) {
    		// 创建databinder	
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {
                if (!mavContainer.isBindingDisabled(name)) {
                    this.bindRequestParameters(binder, webRequest);
                }

                this.validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                    throw new BindException(binder.getBindingResult());
                }
            }

            if (!parameter.getParameterType().isInstance(attribute)) {
                attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
            }

            bindingResult = binder.getBindingResult();
        }
 public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
        WebDataBinder dataBinder = this.createBinderInstance(target, objectName, webRequest);
        if (this.initializer != null) {
        	// 看下这里的初始化方法
            this.initializer.initBinder(dataBinder, webRequest);
        }

        this.initBinder(dataBinder, webRequest);
        return dataBinder;
    }
public void initBinder(WebDataBinder binder) {
		...
        if (this.validator != null && binder.getTarget() != null && this.validator.supports(binder.getTarget().getClass())) {
            binder.setValidator(this.validator);
        }
        ...
    }

原来在这个初始化器里会将属性validator进行赋值

所以问题就落到这个初始化器怎么来的了

在这个MVC配置类中往Bean容器添加了RequestMappingHandlerAdapter,这个类有一个WebBindingInitializer的属性,使用setter方法显示的创建了一个初始化器

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
...
@Bean
	public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
			@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
			@Qualifier("mvcConversionService") FormattingConversionService conversionService,
			@Qualifier("mvcValidator") Validator validator) {

		...
		adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, 
		...
		return adapter;
	}
	...
}
	protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer(
			FormattingConversionService mvcConversionService, Validator mvcValidator) {

		ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
		initializer.setConversionService(mvcConversionService);
		initializer.setValidator(mvcValidator);
		MessageCodesResolver messageCodesResolver = getMessageCodesResolver();
		if (messageCodesResolver != null) {
			initializer.setMessageCodesResolver(messageCodesResolver);
		}
		return initializer;
	}

好的 最后一个问题了,这个RequestMappingHandlerAdapter创建时候,注入了容器中的Validatorbean对象,这个是啥时候塞进容器的呢?

WebMvcAutoConfiguration配置类中又显示的往容器里生成了一个我们的主角

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
	 @Bean
        public Validator mvcValidator() {
            return !ClassUtils.isPresent("javax.validation.Validator", this.getClass().getClassLoader()) ? super.mvcValidator() : ValidatorAdapter.get(this.getApplicationContext(), this.getValidator());
        }
}
  public static Validator get(ApplicationContext applicationContext, Validator validator) {
        return validator != null ? wrap(validator, false) : getExistingOrCreate(applicationContext);
    }
  private static Validator getExistingOrCreate(ApplicationContext applicationContext) {
        Validator existing = getExisting(applicationContext);
        return existing != null ? wrap(existing, true) : create();
    }
private static Validator wrap(Validator validator, boolean existingBean) {
        if (validator instanceof javax.validation.Validator) {
            return validator instanceof SpringValidatorAdapter ? new ValidatorAdapter((SpringValidatorAdapter)validator, existingBean) : new ValidatorAdapter(new SpringValidatorAdapter((javax.validation.Validator)validator), existingBean);
        } else {
            return validator;
        }
    }

所以,Spring Boot帮我们做了很多的整合,几乎是开箱即用。

补充,自定义校验器

文章中提到了这两个注解的使用,结论是只要在接口方法参数上加注解即可,类上不需要加。最近我又碰到了一个问题,这里做下补充:

首先我自定义了一个校验的注解,并自己实现了校验逻辑,代码如下

@Constraint(validatedBy = CustomValidatorConstraint.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface CustomValidator {

    String name();

    boolean notnull() default false;

    int min() default 0;

    int max() default Integer.MAX_VALUE;

    PatternEnum pattern() default PatternEnum.NULL;

    Class<? extends Enum<?>>[] enumClass() default {};

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}
public class CustomValidatorConstraint implements ConstraintValidator<CustomValidator, Object> {

    Object name;

    boolean notnull;

    int max;

    int min;

    Class<? extends Enum<?>>[] enumClass;

    PatternEnum patternEnum;

    @Override
    public void initialize(CustomValidator constraintAnnotation) {
        name = constraintAnnotation.name();
        notnull = constraintAnnotation.notnull();
        enumClass = constraintAnnotation.enumClass();
        max = constraintAnnotation.max();
        min = constraintAnnotation.min();
        patternEnum = constraintAnnotation.pattern();
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {

        if (notnull && value == null) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(name + "异常").addConstraintViolation();
            return false;
        }

        return true;
    }
}

然后我在接口参数上加了该注解,发现不生效
在这里插入图片描述

即使是在加一个@Validated也不行
在这里插入图片描述

最后我在类上边加了一个注解,就可以了
Spring Boot下@Valid和@Validated的区别【源码级】_第6张图片

这是为什么呢?于是我debug了一下,类上加了Spring的注解,接口里加了自己的注解,虽然也实现了校验的功能,但是此时的功能,已经完成了上述的参数绑定时校验的步骤,这个时候校验是在入参结束后执行的拦截器中的参数校验,调用栈如下

还有一种情况就是,接口参数我传的是对象,我需要对对象里边字段做自定义校验,此时需要在接口参数里加上@Valid@Validated任意一个才行,在类上加无效。
Spring Boot下@Valid和@Validated的区别【源码级】_第7张图片

所以要注意,如果用接口里显示使用了自定义的校验器一定记得在类上加@Validated,加@Valid也不行,毕竟是要走Spring的拦截器逻辑,你一个外来的注解他不管,而且此时的校验是在参数入完参之后执行的。

你可能感兴趣的:(Spring,spring,java)