当前位置: 首页 > news >正文

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

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

  • 一. 对象参数校验失效
    • 1.1 案例复现
    • 1.2 原理分析
    • 1.3 问题解决
  • 二. 嵌套对象的校验失效
    • 2.1 案例复现
    • 2.2 原理分析
    • 2.3 问题解决
    • 2.4 总结

一. 对象参数校验失效

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

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注解来表示支持级联校验。

相关文章:

  • 百亿积木市场,能跑出一个“中国乐高”?
  • k8s之Pod
  • SpringBoot_11_整合MyBatis
  • 框架阶段七:SpringSecurity
  • 分布式机器学习---数据与模型划分
  • HDFS、YARN、MapReduce概述及三者之间的关系
  • 商家入驻商城 多商户商城 宝塔安装搭建教程 说明 小程序、h5、pc端
  • 中海庭罗凯:Prometheus监控Argo Workflow云原生工作流的方法
  • JMeter进行并发测试参数化
  • lotus 1.17.1-rc3 calibnet Boost
  • priority_queue(优先级队列的模拟使用和实现)
  • git工作中常用的命令
  • mmdetection--pointpillars-demo运行
  • 使用react-amanda快速搭建管理类型的系统
  • 由于apt-get命令不知道为什么使用不了(可能是关闭了某个访问外网的东西),所以手动在ubuntu上安装mongodb
  • dva中组件的懒加载
  • egg(89)--egg之redis的发布和订阅
  • emacs初体验
  • flask接收请求并推入栈
  • JS实现简单的MVC模式开发小游戏
  • Linux Process Manage
  • mongodb--安装和初步使用教程
  • 官方解决所有 npm 全局安装权限问题
  • 开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?
  • 马上搞懂 GeoJSON
  • 什么软件可以剪辑音乐?
  • 因为阿里,他们成了“杭漂”
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • # 计算机视觉入门
  • #单片机(TB6600驱动42步进电机)
  • (2015)JS ES6 必知的十个 特性
  • (C++)八皇后问题
  • (java版)排序算法----【冒泡,选择,插入,希尔,快速排序,归并排序,基数排序】超详细~~
  • (libusb) usb口自动刷新
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (附源码)springboot建达集团公司平台 毕业设计 141538
  • (附源码)ssm教师工作量核算统计系统 毕业设计 162307
  • (附源码)计算机毕业设计SSM疫情居家隔离服务系统
  • (三)模仿学习-Action数据的模仿
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • (转)大型网站架构演变和知识体系
  • . NET自动找可写目录
  • .equals()到底是什么意思?
  • .NET CF命令行调试器MDbg入门(四) Attaching to Processes
  • .net 开发怎么实现前后端分离_前后端分离:分离式开发和一体式发布
  • .NET 设计模式—适配器模式(Adapter Pattern)
  • .Net高阶异常处理第二篇~~ dump进阶之MiniDumpWriter
  • .net实现头像缩放截取功能 -----转载自accp教程网
  • /var/lib/dpkg/lock 锁定问题
  • @AliasFor注解
  • @RequestBody与@ModelAttribute
  • [ C++ ] STL---string类的使用指南
  • [AutoSar]BSW_Memory_Stack_003 NVM与APP的显式和隐式同步
  • [C#]winform使用引导APSF和梯度自适应卷积增强夜间雾图像的可见性算法实现夜间雾霾图像的可见度增强