Spring Boot项目里对于接口参数校验,可以使用javax.validation.constraints
包下的注解来优雅的校验。比如参数长度、是否为null甚至可以使用正则表达式来校验参数格式,以及校验不通过返回的提示信息都可以通过注解进行配置,实在是方便的很。
但是本人在开发中遇到了这么几个问题:
javax.validation-validation-api
仍旧不能启动,后来导入了hibernate-validator
的依赖就可以了。为什么?controller
上加@Validated注解,同时接口参数里也要加才能使用,really?@Valid
是javax包下的注解,而@Validated
是Spring的注解,在Spring Boot项目里得使用后者才行。确定?基于以上几个问题,我翻阅了不少资料,debug好几轮源码才找到了答案,有兴趣得伙伴可以跟着我一起来看看。
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的依赖关系
它其实还是依赖了hibernate校验器的包,毕竟它是一个成熟的工具了,Spring当然是取其精华,不再自己搞一套了。
值得注意的是,这个jakarta.validation
又是什么鬼?怎么和javax.validation
不一样呢。打开这个包看看,其实是一样的。
这个包其实就是一个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()
的方法,跟进去发现,并没有校验器可以使用,后面就不用说了,自然就是不能够对参数进行校验了。
看到这一步能得出两个结论
@Validated
注解和@Valid
都是可以被识别的,用哪一个都可以,并且只需要在方法参数上加注解就行,类上边不需要加注解。javax.validation-validation-api
这个依赖只提供校验器接口,没有具体的实现,所以不能完成参数校验,所以要完成校验工作还得引入hibernate-validator
的依赖,这里是提供了校验器的实现,我截图给大家看一下现在还剩下一个问题,这个hibernate校验器是什么时候被加载进来的呢?我也没做什么配置啊?
我们继续往下看
是否有校验器可用,关键看DataBinder
类中的校验器list是否不为空
private final List
这个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
创建时候,注入了容器中的Validator
bean对象,这个是啥时候塞进容器的呢?
在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;
}
}
这是为什么呢?于是我debug了一下,类上加了Spring的注解,接口里加了自己的注解,虽然也实现了校验的功能,但是此时的功能,已经完成了上述的参数绑定时校验的步骤,这个时候校验是在入参结束后执行的拦截器中的参数校验,调用栈如下
还有一种情况就是,接口参数我传的是对象,我需要对对象里边字段做自定义校验,此时需要在接口参数里加上@Valid
或@Validated
任意一个才行,在类上加无效。
所以要注意,如果用接口里显示使用了自定义的校验器,一定记得在类上加@Validated
,加@Valid
也不行,毕竟是要走Spring的拦截器逻辑,你一个外来的注解他不管,而且此时的校验是在参数入完参之后执行的。