Spring validation参数校验系列
2、Spring validation参数校验之自定义校验规则及编程式校验等进阶篇
3、【源码】Spring validation参数校验原理解析之Controller控制器参数校验中@RequestBody参数校验实现原理
4、【源码】Spring validation参数校验原理解析之Controller控制器参数校验中@ModelAttribute及实体类参数校验实现原理
5、【源码】Spring validation参数校验原理解析之基本类型参数及Service层方法参数校验实现原理
6、【源码】Spring validation校验的核心类ValidatorImpl、MetaDataProvider和AnnotationMetaDataProvider源码分析
7、Spring validation参数校验高级篇之跨参数校验Cross-Parameter及分组序列校验@GroupSequenceProvider、@GroupSequence
8、【源码】Spring validation参数校验之跨参数校验Cross-Parameter原理分析
9、【源码】Spring validation参数校验之分组序列校验@GroupSequenceProvider、@GroupSequence的实现原理
10、【源码】Spring validation参数校验实现原理总结
前言
《Spring validation参数校验系列》前两篇文章分别分享了Spring validation的基本使用以及自定义校验规则、编程式校验等进阶功能。本文重新另起一篇,从源码分析一下Spring validation中@RequestBody参数校验实现原理,让大家知其然知其所以然。本文基于Spring-5.3.10源码分析。
温馨提醒:
Hibernate validation的设计比较复杂,要一次性全部分析清楚很困难,关联的细节很多。所以《Spring validation参数校验系列》文章通过从整体到细节,在每一篇中,不影响主题内容的情况下,穿插引入一些细节。在分享中,也会暂时忽略一些细节,留在下一篇讲解。建议如果本篇不太理解的,可以看看该系列的上一篇或者下一篇源码讲解文章。
@RequestBody注解参数校验入口
在SpringMVC中,使用RequestResponseBodyMethodProcessor解析带@RequestBody注解的参数以及处理@ResponseBody注解方法的返回值。参数校验的逻辑在resolveArgument()方法,核心代码如下:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@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) {
// 执行数据校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
validateIfApplicable()参数校验
resolveArgument()方法执行父类的validateIfApplicable()方法进行参数校验,代码如下:
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取方法入参添加的注解
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 遍历注解,判断是否为校验相关的注解。不是返回null
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
// 如果是,则执行数据校验
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
2.1 ValidationAnnotationUtils类的代码如下:
public abstract class ValidationAnnotationUtils {
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
@Nullable
public static Object[] determineValidationHints(Annotation ann) {
// 获取注解的类型
Class<? extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
// 如果是@Valid注解,则返回空对象数组
if ("javax.validation.Valid".equals(annotationName)) {
return EMPTY_OBJECT_ARRAY;
}
// 获取@Validated注解对象
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null) {
Object hints = validatedAnn.value();
// 如果有添加@Validated注解,转换@Validated注解的信息为对象数组
return convertValidationHints(hints);
}
// 如果注解的名称中是以Valid字符串开头的也会进行校验
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
return convertValidationHints(hints);
}
return null;
}
private static Object[] convertValidationHints(@Nullable Object hints) {
if (hints == null) {
return EMPTY_OBJECT_ARRAY;
}
return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
ValidationAnnotationUtils的determineValidationHints()方法对注解进行判断,如果是@Validated、@Valid或者Valid字符串开头的注解,返回一个Object[]数组;否则返回null。
注:从这个方法可以发现@Validated和@Valid可以混用。如果是@Validated,返回的数组为value值,即校验分组。不明白的可以看本人的另一篇博客
如果是@Valid,返回空的Object数组。从此处可知只有@Validated注解支持分组校验。
2.2 如果2.1的ValidationAnnotationUtils的determineValidationHints()方法返回不为null,则执行binder.validate(validationHints)进行校验。DataBinder.java的validate()方法如下:
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);
}
}
}
无论是否有分组,最终都是调用ValidatorAdapter的validate()方法。
而后执行SpringValidatorAdapter的validate()方法
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
// 从上面的界面中可知,this.targetValidator为ValidatorImpl对象
processConstraintViolations(
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
从上面的截图可知,this.targetValidator为ValidatorImpl对象,所以执行ValidatorImpl的validate()方法。
ValidatorImpl.validate()
@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
// 数据有效性校验
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
sanityCheckGroups( groups );
@SuppressWarnings("unchecked")
Class<T> rootBeanClass = (Class<T>) object.getClass();
// 获取添加@Validated或者@Valid注解的入参对象类型的BeanMetaData
BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
if ( !rootBeanMetaData.hasConstraints() ) {
return Collections.emptySet();
}
// 将类以及类中添加的约束信息封装成BaseBeanValidationContext对象
BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );
// 校验组生成组和序列的顺序。如果groups为空,则添加Default.class组
ValidationOrder validationOrder = determineGroupValidationOrder( groups );
// 再次封装成BeanValueContext上下文对象
BeanValueContext<?, Object> valueContext = ValueContexts.getLocalExecutionContextForBean(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
// 校验,并返回校验结果
return validateInContext( validationContext, valueContext, validationOrder );
}
3.1 BeanMetaDataManagerImpl的getBeanMetaData(Class<T> beanClass)方法,对应的beanClass首次调用时,会先调用createBeanMetaData()方法,遍历beanClass及其父类,调用BeanMetaDataBuilder.build()方法,创建BeanMetaData对象,并以类名为key,保存到ConcurrentReferenceHashMap对象中缓存起来;再次调用getBeanMetaData()方法时,直接从缓存中获取。
BeanMetaData对象中,解析并报存了bean类中的属性、方法、构造器、方法参数、构造器参数中添加的约束注解的信息。以便后续校验时,可以直接使用。
3.2 determineGroupValidationOrder( groups )通过校验组生成组和序列的顺序时,如果groups为空,则添加Default.class组。即Spring validation校验都是分组校验,只是如果没有指定分组,则为默认分组。
ValidatorImpl.validateInContext()
执行ValidatorImpl的validateInContext()方法进行校验并返回校验结果。在该方法中,遍历校验组,执行validateConstraintsForCurrentGroup()方法。
private void validateConstraintsForCurrentGroup(BaseBeanValidationContext<?> validationContext, BeanValueContext<?, Object> valueContext) {
// 如果不是默认组,则调用validateConstraintsForNonDefaultGroup()直接进行校验
if ( !valueContext.validatingDefault() ) {
validateConstraintsForNonDefaultGroup( validationContext, valueContext );
}
else {
// 如果是默认组,则要判断当前类的父类是否有添加分组
validateConstraintsForDefaultGroup( validationContext, valueContext );
}
}
无论是否是默认组,都会调用validateMetaConstraints()。
private void validateMetaConstraints(BaseBeanValidationContext<?> validationContext, ValueContext<?, Object> valueContext, Object parent,
Iterable<MetaConstraint<?>> constraints) {
// 循环遍历添加的约束,执行validateMetaConstraint()方法。
for ( MetaConstraint<?> metaConstraint : constraints ) {
validateMetaConstraint( validationContext, valueContext, parent, metaConstraint );
// 调用shouldFailFast()判断是否添加了快速失败,且校验失败,则会结束校验过程
if ( shouldFailFast( validationContext ) ) {
break;
}
}
}
4.1 validateMetaConstraint()会调用metaConstraint.validateConstraint( validationContext, valueContext ),最后会调用SimpleConstraintTree.validateConstraints()方法进行校验。
4.2 在shouldFailFast( validationContext )方法中,会调用isFailFastModeEnabled()来判断是否设置了快速失败,该属性为failFast。所以如果要快速失败,在配置文件中添加failFast配置。如下:
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 只要出现校验失败的情况就立即结束校验
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
SimpleConstraintTree.validateConstraints()
在该方法中,获取校验对应的校验器,然后执行校验。
@Override
protected void validateConstraints(ValidationContext<?> validationContext,
ValueContext<?, ?> valueContext,
Collection<ConstraintValidatorContextImpl> violatedConstraintValidatorContexts) {
if ( LOG.isTraceEnabled() ) {
LOG.tracef(
"Validating value %s against constraint defined by %s.",
valueContext.getCurrentValidatedValue(),
descriptor
);
}
// 获取约束对应的约束器
ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );
// create a constraint validator context
ConstraintValidatorContextImpl constraintValidatorContext = validationContext.createConstraintValidatorContextFor(
descriptor, valueContext.getPropertyPath()
);
// 进行校验
if ( validateSingleConstraint( valueContext, constraintValidatorContext, validator ).isPresent() ) {
violatedConstraintValidatorContexts.add( constraintValidatorContext );
}
}
5.1 getInitializedConstraintValidator( validationContext, valueContext )为父类ConstraintTree的方法,该方法会判断当前的校验器是否存在,如果不存在,调用ConstraintValidatorManagerImpl的getInitializedValidator()方法。
@Override
public <A extends Annotation> ConstraintValidator<A, ?> getInitializedValidator(
Type validatedValueType,
ConstraintDescriptorImpl<A> descriptor,
ConstraintValidatorFactory constraintValidatorFactory,
HibernateConstraintValidatorInitializationContext initializationContext) {
// 参数有效性检查
Contracts.assertNotNull( validatedValueType );
Contracts.assertNotNull( descriptor );
Contracts.assertNotNull( constraintValidatorFactory );
Contracts.assertNotNull( initializationContext );
CacheKey key = new CacheKey( descriptor.getAnnotationDescriptor(), validatedValueType, constraintValidatorFactory, initializationContext );
@SuppressWarnings("unchecked")
// 先从缓存中获取对应约束的约束器
ConstraintValidator<A, ?> constraintValidator = (ConstraintValidator<A, ?>) constraintValidatorCache.get( key );
if ( constraintValidator == null ) {
// 如果没有对应的约束器,则创建并初始化约束器
constraintValidator = createAndInitializeValidator( validatedValueType, descriptor, constraintValidatorFactory, initializationContext );
// 将约束器添加到缓存
constraintValidator = cacheValidator( key, constraintValidator );
}
else {
LOG.tracef( "Constraint validator %s found in cache.", constraintValidator );
}
return DUMMY_CONSTRAINT_VALIDATOR == constraintValidator ? null : constraintValidator;
}
先从缓存获取对应约束的约束器,如果没有,执行父类的AbstractConstraintValidatorManagerImpl.createAndInitializeValidator()方法,创建约束器,并进行初始化,执行初始化方法。
protected <A extends Annotation> ConstraintValidator<A, ?> createAndInitializeValidator(
Type validatedValueType,
ConstraintDescriptorImpl<A> descriptor,
ConstraintValidatorFactory constraintValidatorFactory,
HibernateConstraintValidatorInitializationContext initializationContext) {
ConstraintValidatorDescriptor<A> validatorDescriptor = findMatchingValidatorDescriptor( descriptor, validatedValueType );
ConstraintValidator<A, ?> constraintValidator;
if ( validatorDescriptor == null ) {
return null;
}
// 通过校验器的描述符,newInstance()获得一个校验器对象
constraintValidator = validatorDescriptor.newInstance( constraintValidatorFactory );
// 执行校验器的initialize()进行初始化
initializeValidator( descriptor, constraintValidator, initializationContext );
return constraintValidator;
}
该方法会调用AbstractConstraintValidatorManagerImpl的initializeValidator()方法。
private <A extends Annotation> void initializeValidator(
ConstraintDescriptor<A> descriptor,
ConstraintValidator<A, ?> constraintValidator,
HibernateConstraintValidatorInitializationContext initializationContext) {
try {
if ( constraintValidator instanceof HibernateConstraintValidator ) {
( (HibernateConstraintValidator<A, ?>) constraintValidator ).initialize( descriptor, initializationContext );
}
// 执行约束器的初始化方法
constraintValidator.initialize( descriptor.getAnnotation() );
}
catch (RuntimeException e) {
if ( e instanceof ConstraintDeclarationException ) {
throw e;
}
throw LOG.getUnableToInitializeConstraintValidatorException( constraintValidator.getClass(), e );
}
}
5.2 执行父类ConstraintTree的validateSingleConstraint( valueContext, constraintValidatorContext, validator )进行校验。
protected final <V> Optional<ConstraintValidatorContextImpl> validateSingleConstraint(
ValueContext<?, ?> valueContext,
ConstraintValidatorContextImpl constraintValidatorContext,
ConstraintValidator<A, V> validator) {
boolean isValid;
try {
@SuppressWarnings("unchecked")
// 获取当前的值
V validatedValue = (V) valueContext.getCurrentValidatedValue();
// 执行校验器的isValid()方法
isValid = validator.isValid( validatedValue, constraintValidatorContext );
}
catch (RuntimeException e) {
if ( e instanceof ConstraintDeclarationException ) {
throw e;
}
throw LOG.getExceptionDuringIsValidCallException( e );
}
if ( !isValid ) {
// 校验失败,返回constraintValidatorContext
return Optional.of( constraintValidatorContext );
}
return Optional.empty();
}
看到这里是不是很熟悉了,如果还不清楚,可以看看
这篇博客中提到的自定义校验规则的部分。
package com.jingai.validation.custom;
import org.apache.logging.log4j.util.Strings;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 验证给定的String值是否满足InEnum注解中的约束
*/
public class InEnumValidator implements ConstraintValidator<InEnum, String> {
private Class<? extends BaseEnum> enumType;
private String enumValues = Strings.EMPTY;
private Object obj = new Object();
/**
* 初始化方法可以获得该校验器对应的注解,从而获取注解中的信息。在校验方法调用前会调用
*/
@Override
public void initialize(InEnum constraintAnnotation) {
enumType = constraintAnnotation.enumType();
}
/**
* 校验。成功返回true,失败返回false
*/
@Override
public boolean isValid(String str, ConstraintValidatorContext constraintValidatorContext) {
System.out.println("in enum str : " + str);
if(!StringUtils.hasText(str)) {
return true;
}
synchronized (enumValues) {
if(enumType == null || !enumType.isEnum()) {
return true;
}
for (BaseEnum e :enumType.getEnumConstants()){
System.out.println("in enum str : " + str + ", e : " + e.getCode() + ", " + e.getDisplay());
if(e.getCode().equals(str)) {
return true;
}
}
if(!StringUtils.hasText(enumValues)) {
for (BaseEnum e :enumType.getEnumConstants()){
enumValues += ";" + e.getCode() + ":" + e.getDisplay();
}
enumValues = enumValues.substring(1);
}
// 返回动态的校验失败信息。如果不添加如下两句代码,返回注解中的message信息
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext.buildConstraintViolationWithTemplate("值" + str + "没在枚举【" + enumValues + "】中")
.addConstraintViolation();
return false;
}
}
}
通过上面的5.2中ConstraintTree的validateSingleConstraint()方法,当调用isValid()校验失败时,会返回constraintValidatorContext,所以在isValid()方法中,也可以通过constraintValidatorContext定义动态的校验失败信息。如上的代码,当性别不在枚举范围时,提示如下。
如果想要有更友好的提示,可以在使用@InEnum注解的message中添加占位符,在此时动态的替换占位符的信息。
自定义校验规则的调用栈信息
SpringValidatorAdapter.processConstraintViolations()
当执行完ValidatorImpl.validate()进行参数校验,校验结束后,会执行SpringValidatorAdapter的processConstraintViolations(),如果校验失败,会将错误信息添加到BindingResult对象中。然后返回到RequestResponseBodyMethodProcessor的resolveArgument()方法。
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@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) {
// 执行数据校验,如果校验失败,会将失败信息添加到binder对象中
validateIfApplicable(binder, parameter);
// 如果有错误信息,则抛MethodArgumentNotValidException异常
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
总结
以上为本次分享的从源码分析Spring validation中带@RequestBody注解的参数的校验原理,Spring validation使用的是Hibernate的validation,里面的细节很多,本人也没有全部研究明白,此处只分享了核心的部分,通过这些能够对@RequestBody注解的参数校验有更直观和深入的了解。
关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。