栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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:


  org.springframework.boot
  spring-boot-starter-validation

看下这个dependency的依赖关系

它其实还是依赖了hibernate校验器的包,毕竟它是一个成熟的工具了,Spring当然是取其精华,不再自己搞一套了。
值得注意的是,这个jakarta.validation又是什么鬼?怎么和javax.validation不一样呢。打开这个包看看,其实是一样的。

javax.validation-validation-api

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

        
            javax.validation
            validation-api
            2.0.1.Final
        

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

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

public interface Validator {

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

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

	 Set> validatevalue(Class beanType,
												  String propertyName,
												  Object value,
												  Class... groups);

	BeanDescriptor getConstraintsForClass(Class clazz);

	 T unwrap(Class 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()的方法,跟进去发现,并没有校验器可以使用,后面就不用说了,自然就是不能够对参数进行校验了。

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

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

    这个校验器才是真正干活的。

我也把调用栈放上来,供大家参考

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帮我们做了很多的整合,几乎是开箱即用。

可能描述的比较乱,有问题留言哈。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/531533.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号