Spring常见问题解决 - 对象参数校验失效

30 篇文章 3 订阅

一. 对象参数校验失效

我们有时候需要对接口中传入的参数做出校验,我们往往会通过在类对象的属性上添加校验性的注解,来完成快捷的规则校验。下面给出案例。

1.1 案例复现

1.pom依赖:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.自定义一个Student类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student{
    @Size(max = 10)
    private String name;
    private Integer age;
}

3.Controller类:

@RestController
public class MyController {
    @PostMapping("/hello")
    public Student hello(@RequestBody Student student) {
        return student;
    }
}

4.请求测试:
在这里插入图片描述

我们可以发现,结果并不是我们预想到的,我们对name传的值的长度很明显超过了10,但是程序并没有进行校验拦截。反而是正常的运行并输出。那么到底是什么原因导致这样的结果呢?

1.2 原理分析

这里就要从Spring对于HTTP请求的处理,即请求体-->Java对象的转换过程来说了。我在Spring常见问题解决 - Body返回体中对应值为null的不输出?这篇文章里面提到过,关于请求体的转换过程,有一段关键的代码,如下:

public class InvocableHandlerMethod extends HandlerMethod {
	@Nullable
	public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {
		// 1.获取参数值
		Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
		if (logger.isTraceEnabled()) {
			logger.trace("Arguments: " + Arrays.toString(args));
		}
		// 2.再对参数进行对象转换操作
		return doInvoke(args);
	}
}

第一步是关于参数值的一个解析的,对于本文案例来说,就是获取Student实例对象,参数校验也应该发生在这个阶段。因为第二个步骤只是通过反射机制来执行一遍方法而已。我们在第一步中往深入走:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {	
	try {
		// 通过解析器来解析参数,进行参数绑定
		args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
	}
}
↓↓↓↓↓↓↓
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
		// 这里去遍历所有支持的解析器,去找到合适的解析器
		HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
		if (resolver == null) {
			throw new IllegalArgumentException("Unsupported parameter type [" +
					parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
		}
		return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
	}
	↓↓↓↓↓↓↓
	@Nullable
	private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
				if (resolver.supportsParameter(parameter)) {
					result = resolver;
					this.argumentResolverCache.put(parameter, result);
					break;
				}
			}
		}
		return result;
	}
}

那么我们知道,我们在Controller层中,对于Student参数的接收,我们加入了@RequestBody这个注解,那么我们再来debug看下:

在这里插入图片描述
判断条件就是是否添加了@RequestBody这个注解。关键代码在于:

if (resolver.supportsParameter(parameter)) 
↓↓↓↓↓↓↓
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.hasParameterAnnotation(RequestBody.class);
	}
}

找到了合适的解析器之后,就应该进行值和对象的装配过程了。

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
↓↓↓↓↓↓↓
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		// 1.消息的转换,读取请求体,转化为对应的Java对象.这里获得的结果是User对象
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		// 2.获取参数名,这里获得的结果是 user
		String name = Conventions.getVariableNameForParameter(parameter);
		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				// 3.参数的校验过程
				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

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
			// 判断是否需要校验
			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;
			}
		}
	}
}

从这里我们看出了一个问题就是,Spring中,对于请求体的参数校验,是需要满足一定的条件的:

  • 代码里需要拥有@Validated注解。
  • 或者注解的名称需要以Valid为开头。

那么知道问题出在哪了,我们的Controller层代码中,上述的两个条件一个都不满足,那么我们就针对源码来解决。

1.3 问题解决

第一种:添加@Validted注解。

@RestController
public class MyController {
    @PostMapping("/hello")
    public Student hello(@Validated @RequestBody Student student) {
        return student;
    }
}

再次访问接口,结果如下:
在这里插入图片描述
控制台输出:可见校验是成功了。
在这里插入图片描述

第二种:自定义一个注解,以Valid开头:

@Retention(RetentionPolicy.RUNTIME)
public @interface ValidName {
}

@PostMapping("/hello")
public Student hello(@ValidName @RequestBody Student student) {
    return student;
}

效果是一样的,就不做展示了。

二. 嵌套对象的校验失效

第一个大问题解决好后,我们在其基础上再来看一个问题,我们在Student对象里面定义一个Teacher类,然后再Teacher类里面再进行一个参数校验:

2.1 案例复现

1.Teacher类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    @Size(max = 5, message = "教师名称长度不能超过5位")
    private String teacherName;
}

2.Studeng类修改:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    @Size(max = 10, message = "姓名长度不能超过10")
    private String name;
    private Integer age;
    private Teacher teacher;
}

3.Controller代码:

@RestController
public class MyController {
    @PostMapping("/hello")
    public Student hello(@Validated @RequestBody Student student) {
        return student;
    }
}

4.发送请求,首先我们来看一个正常的请求:
在这里插入图片描述
然后我们给teacher的名字传的长一点,再看下校验是否通过:

在这里插入图片描述

我们可以看到,校验规则失效了。那么这是为什么呢?

2.2 原理分析

我们来看请求体转换过程中,对于参数校验的的这段代码:

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
	Annotation[] annotations = parameter.getParameterAnnotations();
	for (Annotation ann : annotations) {
		Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
		// 这里我们代码已经满足了对应的条件,添加了@Validted注解
		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;
		}
	}
}
↓↓↓↓↓↓
public void validate(Object... validationHints) {
	Object target = getTarget();
	Assert.state(target != null, "No target to validate");
	// 首先根据目标的类型定义找出所有的校验点
	BindingResult bindingResult = getBindingResult();
	// 对每一个校验器执行验证过程
	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);
		}
	}
}

我们关注validate()的逻辑:(注意包名)

package org.hibernate.validator.internal.engine;

public class ValidatorImpl implements Validator, ExecutableValidator {
	@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();
		// 寻找需要进行校验的元数据信息
		BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
		// ...
		return validateInContext( validationContext, valueContext, validationOrder );
	}
}

由于代码比较多,我这里贴出调用栈,看下最终的是如何判断元数据信息需要被校验的:从上述代码beanMetaDataManager.getBeanMetaData( rootBeanClass );开始:
在这里插入图片描述

最终定位到:AnnotationMetaDataProvider .getCascadingMetaData()

package org.hibernate.validator.internal.metadata.provider;
public class AnnotationMetaDataProvider implements MetaDataProvider {
	private CascadingMetaDataBuilder getCascadingMetaData(JavaBeanAnnotatedElement annotatedElement,
			Map<TypeVariable<?>, CascadingMetaDataBuilder> containerElementTypesCascadingMetaData) {
		return CascadingMetaDataBuilder.annotatedObject( annotatedElement.getType(), 
		annotatedElement.isAnnotationPresent( Valid.class ),
		containerElementTypesCascadingMetaData, 
		getGroupConversions( annotatedElement.getAnnotatedType() ) );
	}
}

意思就是:会根据成员字段是否标记了 @Valid 来决定这个字段以后是否做级联校验(即嵌套类字段)。

2.3 问题解决

很简单,teacher字段上添加一个@Valid注解。表示该字段需要做级联校验。
在这里插入图片描述
结果如下:
在这里插入图片描述

控制台输出:
在这里插入图片描述

2.4 总结

  1. 进行参数校验的时候,请在Controller参数入口处,增加@Validted注解。
  2. 倘若有级联属性,类里面嵌套类。需要在对应的属性上增加@Valid注解来表示支持级联校验。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值