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


    一. 对象参数校验失效

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

    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.自定义一个Student类:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Student{
        @Size(max = 10)
        private String name;
        private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.Controller类:

    @RestController
    public class MyController {
        @PostMapping("/hello")
        public Student hello(@RequestBody Student student) {
            return student;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    第一步是关于参数值的一个解析的,对于本文案例来说,就是获取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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    那么我们知道,我们在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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    我们看第三步,参数校验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;
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

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

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

    1.3 问题解决

    第一种:添加@Validted注解。

    @RestController
    public class MyController {
        @PostMapping("/hello")
        public Student hello(@Validated @RequestBody Student student) {
            return student;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    @Retention(RetentionPolicy.RUNTIME)
    public @interface ValidName {
    }
    
    @PostMapping("/hello")
    public Student hello(@ValidName @RequestBody Student student) {
        return student;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    二. 嵌套对象的校验失效

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

    2.1 案例复现

    1.Teacher类:

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

    2.Studeng类修改:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Student {
        @Size(max = 10, message = "姓名长度不能超过10")
        private String name;
        private Integer age;
        private Teacher teacher;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.Controller代码:

    @RestController
    public class MyController {
        @PostMapping("/hello")
        public Student hello(@Validated @RequestBody Student student) {
            return student;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    我们关注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 );
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    由于代码比较多,我这里贴出调用栈,看下最终的是如何判断元数据信息需要被校验的:从上述代码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() ) );
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    2.3 问题解决

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

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

    2.4 总结

    1. 进行参数校验的时候,请在Controller参数入口处,增加@Validted注解。
    2. 倘若有级联属性,类里面嵌套类。需要在对应的属性上增加@Valid注解来表示支持级联校验。
  • 相关阅读:
    制作一个用户登录界面
    基于github_monitor的源代码监控+自动化监控任务生成+webhook实现飞书机器人通知
    CSP-S2023 总结
    大厂的 404 页面都长啥样?看到最后一个,我笑了~
    LeetCode(17)罗马数字转整数【数组/字符串】【简单】
    VMware虚拟机部署Linux Ubuntu系统的方法
    【linux】 第1回 linux运维基础
    取多条数据相同用户时间最小的那条数据
    WebAssembly跨平台开发
    云端技术驾驭DAY12——Pod调度策略、Pod标签管理、Pod资源配额与限额、全局资源配额与限额策略
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126649671