• springboot情操陶冶-web配置(七)


    参数校验通常是OpenApi必做的操作,其会对不合法的输入做统一的校验以防止恶意的请求。本文则对参数校验这方面作下简单的分析

    spring.factories

    读者应该对此文件加以深刻的印象,很多springboot整合第三方插件的方式均是从此配置文件去读取的,本文关注下检验方面的东西。在相应的文件搜索validation关键字,最终定位至ValidationAutoConfiguration类,笔者这就针对此类作主要的分析

    ValidationAutoConfiguration

    优先看下其头上的注解

    @Configuration
    @ConditionalOnClass(ExecutableValidator.class)
    @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
    @Import(PrimaryDefaultValidatorPostProcessor.class)
    

    使此类成功被注册的条件有两个,第一是当前环境下存在ExecutableValidator类,第二是当前类环境存在META-INF/services/javax.validation.spi.ValidationProvider文件。
    通过查看maven依赖得知,其实springboot在引入starter-web板块便引入了hibernate-validator包,此包便满足了上述的两个要求。
    笔者发现其也引入了PrimaryDefaultValidatorPostProcessor类,主要是判断当前的bean工厂是否已经包含了LocalValidatorFactoryBeanValidator对象,不影响大局。即使没有配置,下述的代码也是会注册的

    	@Bean
    	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    	@ConditionalOnMissingBean(Validator.class)
    	public static LocalValidatorFactoryBean defaultValidator() {
    		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
    		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
    		factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
    		return factoryBean;
    	}
    
    	@Bean
    	@ConditionalOnMissingBean
    	public static MethodValidationPostProcessor methodValidationPostProcessor(
    			Environment environment, @Lazy Validator validator) {
    		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
    		boolean proxyTargetClass = environment
    				.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
    		processor.setProxyTargetClass(proxyTargetClass);
    		processor.setValidator(validator);
    		return processor;
    	}
    

    通过查阅代码得知,使用注解式的校验方式是通过添加@Validated注解来实现的,但是其作用于参数上还是类上是有不同的操作逻辑的。笔者将之区分开,方便后续查阅。先附上@Validated注解源码

    @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Validated {
    
    	/**
    	 * Specify one or more validation groups to apply to the validation step
    	 * kicked off by this annotation.
    	 * <p>JSR-303 defines validation groups as custom annotations which an application declares
    	 * for the sole purpose of using them as type-safe group arguments, as implemented in
    	 * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
    	 * <p>Other {@link org.springframework.validation.SmartValidator} implementations may
    	 * support class arguments in other ways as well.
    	 */
    	Class<?>[] value() default {};
    
    }
    

    类级别的校验

    @Validated作用于类上,其相关的处理逻辑便是由MethodValidationPostProcessor来实现的,笔者稍微看下关键源码方法afterPropertiesSet()

    	@Override
    	public void afterPropertiesSet() {
    		// 查找对应的类以及祖先类上是否含有@Validated注解
    		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
    		// 创建MethodValidationInterceptor处理类来处理具体的逻辑
    		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    	}
    

    上述的配置表明只要某个类上使用了@Validated注解,其相应的方法就会被校验相关的参数。笔者紧接着看下MethodValidationInterceptor#invoke()方法

    	@Override
    	@SuppressWarnings("unchecked")
    	public Object invoke(MethodInvocation invocation) throws Throwable {
    		// 读取相应方法上的@Validated的value属性,为空也是没问题的
    		Class<?>[] groups = determineValidationGroups(invocation);
    
    		// Standard Bean Validation 1.1 API
    		ExecutableValidator execVal = this.validator.forExecutables();
    		Method methodToValidate = invocation.getMethod();
    		Set<ConstraintViolation<Object>> result;
    
    		try {
    			// ①校验参数
    			result = execVal.validateParameters(
    					invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
    		}
    		catch (IllegalArgumentException ex) {
    			// ②校验对应的桥接方法(兼容jdk1.5+后的泛型用法)的参数
    			methodToValidate = BridgeMethodResolver.findBridgedMethod(
    					ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
    			result = execVal.validateParameters(
    					invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
    		}
    		if (!result.isEmpty()) {
    			throw new ConstraintViolationException(result);
    		}
    		// ③校验对应的返回值
    		Object returnValue = invocation.proceed();
    
    		result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
    		if (!result.isEmpty()) {
    			throw new ConstraintViolationException(result);
    		}
    
    		return returnValue;
    	}
    

    只要类上使用了@Validated注解,则其下的所有方法都会被校验。
    检验规则如下:参数返回值都会被校验,只要某一个没有通过,则会抛出ConstraintViolationException异常以示警告。
    具体的参数校验属于hibernate-validator的范畴了,感兴趣的读者可自行分析~

    参数级别的校验(推荐)

    @Validated注解作用于方法的参数上,其有关的校验则是被springmvc的参数校验器处理的。笔者在ModelAttributeMethodProcessor#resolveArgument()方法中查找到了相应的蛛丝马迹,列出关键的代码

    	@Override
    	@Nullable
    	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
    		....
    		Object attribute = null;
    		BindingResult bindingResult = null;
    
    		if (mavContainer.containsAttribute(name)) {
    			attribute = mavContainer.getModel().get(name);
    		}
    		else {
    			// Create attribute instance
    			try {
    				attribute = createAttribute(name, parameter, binderFactory, webRequest);
    			}
    			catch (BindException ex) {
    				.....
    			}
    		}
    
    		if (bindingResult == null) {
    			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    			if (binder.getTarget() != null) {
    				if (!mavContainer.isBindingDisabled(name)) {
    					bindRequestParameters(binder, webRequest);
    				}
    				// 就是这里
    				validateIfApplicable(binder, parameter);
    				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
    					throw new BindException(binder.getBindingResult());
    				}
    			}
    			// Value type adaptation, also covering java.util.Optional
    			if (!parameter.getParameterType().isInstance(attribute)) {
    				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    			}
    			bindingResult = binder.getBindingResult();
    		}
    
    		....
    
    		return attribute;
    	}
    

    我们继续看下其下的validateIfApplicable()方法

    	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    		// 对参数上含有@Validated注解的进行校验器解析
    		Annotation[] annotations = parameter.getParameterAnnotations();
    		for (Annotation ann : annotations) {
    			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
                            // 兼容@Valid注解方式
    			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;
    			}
    		}
    	}
    

    上述的代码已经很简明概要了,笔者就不展开了。当然如果用户想要在出现异常的时候进行友好的返回,建议参考springboot情操陶冶-web配置(五)的异常机制文章便可迎刃而解

    小结

    参数的校验一般都是结合spring-context板块内的@Validated注解搭配hibernate的校验器便完成了相应的检测功能。
    经过笔者的测试,发现在类上使用@Validated注释基本没啥用,还是会依赖在参数上添加@Validated注解方可生效。
    如果大家对此有什么补充欢迎留言,在此希望此篇对大家有所帮助

  • 相关阅读:
    apt 安装 Oracle Java JDK
    apt 安装 tomcat
    apt安装mysql
    yum 常用命令
    Ubuntu apt-get 更换源
    没有解决不了的bug,静下心一步步排查,早上一个小时就找出问题了
    关于看书学习的一点感悟
    利用暗时间看好了六大原则,下面开始练习23种设计模式
    养成看书思考的习惯
    凡事预则立,有时候还是得不断的去巩固一些基础知识的
  • 原文地址:https://www.cnblogs.com/question-sky/p/9984860.html
Copyright © 2020-2023  润新知