zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Spring提供的@Validated和MethodValidationPostProcessor完成数据校验

Spring数据 完成 提供 校验
2023-09-14 09:13:36 时间


@Validated怎么用

@Validated(Default.class)
public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

// 实现类如下
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public Object hello(Integer id, String name) {
        return null;
    }
}

向容器里注册一个处理器:

@Configuration
public class RootConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

测试:

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = AipsiCwsSupplierServiceApplication.class)
public class TestSpringBean {
    @Autowired
    private HelloService helloService;

    @Test
    public void test1() {
        System.out.println(helloService.getClass());
        helloService.hello(1, null);
    }
}

结果如图:
在这里插入图片描述

完美的校验住了方法入参。

注意此处的一个小细节:若你自己运行这个案例你得到的参数名称可能是hello.args0等,而我此处是形参名。是因为我使用Java8的编译参数:-parameters(此处说一点:若你的逻辑中强依赖于此参数,务必在你的maven中加入编译插件并且配置好此编译参数)


若需要校验方法返回值,改写如下:

    @NotNull
    Object hello(Integer id);

	// 此种写法效果同上
    //@NotNull Object hello(Integer id);

运行:

javax.validation.ConstraintViolationException: hello.<return value>: 不能为null
...

校验完成。就这样借助Spring+JSR相关约束注解,就非常简单明了,语义清晰的优雅的完成了方法级别(入参校验、返回值校验)的校验。

校验不通过的错误信息,再来个全局统一的异常处理,就能让整个工程都能尽显完美之势。(错误消息可以从异常ConstraintViolationException的getConstraintViolations()方法里获得的)


原理时间到

MethodValidationPostProcessor

它是Spring提供的来实现基于方法Method的JSR校验的核心处理器~它能让约束作用在方法入参、返回值上,如:

public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)

官方说明:方法里写有JSR校验注解要想其生效的话,要求类型级别上必须使用@Validated标注(还能指定验证的Group)

这里的方法指的是非controller层的方法,如果是controller的方法,会在数据绑定,即DataBinder处理过程中完成数据校验,这也是为什么我们平时在controller方法上使用约束注解的时候,不需要加@Validated的原因


源码解析

要讲清楚MethodValidationPostProcessor,就必须要从ProxyProcessorSupport说起,我们可以看一下ProxyProcessorSupport的继承体系。

关于AOP部分,在我的Spring源码剖析专栏,已经进行了详细的分析,这里就不多展开了,但是之前一直讲的都是被green圈起来的那部分自动代理创建器,因此本节源码分析重点是在被red圈起来的这部分。
在这里插入图片描述
先从AbstractAdvisingBeanPostProcessor讲起。


AbstractAdvisingBeanPostProcessor—尝试利用内部advisor对bean进行dialing

AbstractAdvisingBeanPostProcessor继承了ProxyProcessorSupport 并且实现了BeanPostProcessor 接口,该类主要就是利用其内部的Advisor 来尝试对bean进行代理。

public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {
    //内部的增强器  
	@Nullable
	protected Advisor advisor;
    //如果当前bean是被代理过的,并且当前bean满足内部Advisor的增强条件,那么需要将内部Advisor加入到被代理bean的
    //Advisor集合中哪一个位置中去
    //如果下面这个标记为真,那就放到第一个位置
	protected boolean beforeExistingAdvisors = false;
    //缓存作用: 对于多例bean而言,每一次获取都需要进行增强判断,太麻烦,这里直接缓存,就可以提高效率
	private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);
    
	public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) {
		this.beforeExistingAdvisors = beforeExistingAdvisors;
	}


	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) {
		return bean;
	}
    
    //在相关初始化方法被调用后,调用该回调接口 
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
	//如果内部没有设置增强器或者bean是spring内部的bean,那么就跳过
		if (this.advisor == null || bean instanceof AopInfrastructureBean) {
			// Ignore AOP infrastructure such as scoped proxies.
			return bean;
		}
       //如果当前bean已经被代理过了
		if (bean instanceof Advised) {
			Advised advised = (Advised) bean;
			//AopUtils.getTargetClass(bean)拿到被代理的目标对象类型,然后利用isEligible方法,判断是否需要被当前内部的advisor增强
			if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
				// Add our local Advisor to the existing proxy's Advisor chain...
				//如果需要被内部的advisor增强的话,再判断内部的这个advisor应该加入当前代理对象对应advisor集合中哪一个位置
				if (this.beforeExistingAdvisors) {
					advised.addAdvisor(0, this.advisor);
				}
				else {
					advised.addAdvisor(this.advisor);
				}
				return bean;
			}
		}
         //如果当前bean没有被代理,那就还是判断当前bean是否需要被增强
		if (isEligible(bean, beanName)) {
		//如果需要,那么准备为当前bean创建代理对象
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			//isProxyTargetClass为真,说明需要采用cglib代理
			if (!proxyFactory.isProxyTargetClass()) {
			//采用jdk代理,将代理对象需要实现的接口,加入proxyFactory对应的接口集合中
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
			//将当前增强器加入
			proxyFactory.addAdvisor(this.advisor);
			//钩子接口---子类覆写可以增添额外的逻辑
			customizeProxyFactory(proxyFactory);

			// Use original ClassLoader if bean class not locally loaded in overriding class loader
			ClassLoader classLoader = getProxyClassLoader();
			if (classLoader instanceof SmartClassLoader && classLoader != bean.getClass().getClassLoader()) {
				classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
			}
			//创建代理
			return proxyFactory.getProxy(classLoader);
		}

		// No proxy needed.
		return bean;
	}

    //判断当前bean是否需要被增强
	protected boolean isEligible(Object bean, String beanName) {
		return isEligible(bean.getClass());
	}

	protected boolean isEligible(Class<?> targetClass) {
		//缓存中有的话,说明是需要被增加的
		Boolean eligible = this.eligibleBeans.get(targetClass);
		if (eligible != null) {
			return eligible;
		}
		//没有增强的话,那就不需要
		if (this.advisor == null) {
			return false;
		}
		//利用advisor内部的pointcut进行判断
		eligible = AopUtils.canApply(this.advisor, targetClass);
		this.eligibleBeans.put(targetClass, eligible);
		return eligible;
	}
	
	protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);
		proxyFactory.setTarget(bean);
		return proxyFactory;
	}


	protected void customizeProxyFactory(ProxyFactory proxyFactory) {
	}
}

这部分与Spring AOP联系密切,如果没搞懂的话,可以先去看一下我的AOP源码解析,在我的Spring 源码解析专栏中


AbstractBeanFactoryAwareAdvisingPostProcessor----结合IOC

AbstractBeanFactoryAwareAdvisingPostProcessor该类基本没有做什么额外多的逻辑,因此大家也没必要太在意。

public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor
		implements BeanFactoryAware {

	@Nullable
	private ConfigurableListableBeanFactory beanFactory;


	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory ?
				(ConfigurableListableBeanFactory) beanFactory : null);
	}

	@Override
	protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
		if (this.beanFactory != null) {
			AutoProxyUtils.exposeTargetClass(this.beanFactory, beanName, bean.getClass());
		}

		ProxyFactory proxyFactory = super.prepareProxyFactory(bean, beanName);
		if (!proxyFactory.isProxyTargetClass() && this.beanFactory != null &&
		//如果bean对应的beanDefinition中的PRESERVE_TARGET_CLASS_ATTRIBUTE属性被设置了,说明也需要采用cglib代理
				AutoProxyUtils.shouldProxyTargetClass(this.beanFactory, beanName)) {
			proxyFactory.setProxyTargetClass(true);
		}
		return proxyFactory;
	}

	@Override
	protected boolean isEligible(Object bean, String beanName) {
		return (!AutoProxyUtils.isOriginalInstance(beanName, bean.getClass()) &&
				super.isEligible(bean, beanName));
	}

}

MethodValidationPostProcessor

// @since 3.1
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
	// 备注:此处你标注@Valid是无用的~~~Spring可不提供识别
	// 当然你也可以自定义注解(下面提供了set方法~~~)
	// 但是注意:若自定义注解的话,此注解只决定了是否要代理,并不能指定分组哦  so,没啥事别给自己找麻烦吧
	private Class<? extends Annotation> validatedAnnotationType = Validated.class;
	// 这个是javax.validation.Validator
	@Nullable
	private Validator validator;

	// 可以自定义生效的注解
	public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
		Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
		this.validatedAnnotationType = validatedAnnotationType;
	}

	// 这个方法注意了:你可以自己传入一个Validator,并且可以是定制化的LocalValidatorFactoryBean哦~(推荐)
	public void setValidator(Validator validator) {
		// 建议传入LocalValidatorFactoryBean功能强大,从它里面生成一个验证器出来靠谱
		if (validator instanceof LocalValidatorFactoryBean) {
			this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
		} else if (validator instanceof SpringValidatorAdapter) {
			this.validator = validator.unwrap(Validator.class);
		} else {
			this.validator = validator;
		}
	}
	// 当然,你也可以简单粗暴的直接提供一个ValidatorFactory即可~
	public void setValidatorFactory(ValidatorFactory validatorFactory) {
		this.validator = validatorFactory.getValidator();
	}


	// 毫无疑问,Pointcut使用AnnotationMatchingPointcut,并且支持内部类哦~
	// 说明@Aysnc使用的也是AnnotationMatchingPointcut,只不过因为它支持标注在类上和方法上,所以最终是组合的ComposablePointcut
	
	// 至于Advice通知,此处一样的是个`MethodValidationInterceptor`~~~~
	@Override
	public void afterPropertiesSet() {
	//AnnotationMatchingPointcut默认只会进行类匹配,去判断当前类上是否标注了对应的注解
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}
	
	// 这个advice就是给@Validation的类进行增强的~  说明:子类可以覆盖哦~
	// @since 4.2
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}
}

它是个普通的BeanPostProcessor,为Bean创建的代理的时机是postProcessAfterInitialization(),也就是在Bean完成初始化后有必要的话用一个代理对象返回进而交给Spring容器管理~(同@Aysnc)

容易想到,关于校验方面的逻辑不在于它,而在于切面的通知:MethodValidationInterceptor


MethodValidationInterceptor

它是AOP联盟类型的通知,此处专门用于处理方法级别的数据校验。

注意理解方法级别:方法级别的入参有可能是各种平铺的参数、也可能是一个或者多个对象

// @since 3.1  因为它校验Method  所以它使用的是javax.validation.executable.ExecutableValidator
public class MethodValidationInterceptor implements MethodInterceptor {

	// javax.validation.Validator
	private final Validator validator;

	// 如果没有指定校验器,那使用的就是默认的校验器
	public MethodValidationInterceptor() {
		this(Validation.buildDefaultValidatorFactory());
	}
	public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
		this(validatorFactory.getValidator());
	}
	public MethodValidationInterceptor(Validator validator) {
		this.validator = validator;
	}


	@Override
	@SuppressWarnings("unchecked")
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
		// 如果是FactoryBean.getObject() 方法  就不要去校验了~
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}

		Class<?>[] groups = determineValidationGroups(invocation);

		// Standard Bean Validation 1.1 API  ExecutableValidator是1.1提供的
		ExecutableValidator execVal = this.validator.forExecutables();
		Method methodToValidate = invocation.getMethod();
		Set<ConstraintViolation<Object>> result; // 错误消息result  若存在最终都会ConstraintViolationException异常形式抛出

		try {
			// 先校验方法入参
			result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		} catch (IllegalArgumentException ex) {
			// 此处回退了异步:找到bridged method方法再来一次
			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注解  从里面拿到分组信息
	// 备注:虽然代理只能标注在类上,但是分组可以标注在类上和方法上哦~~~~ 
	protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
		Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
		if (validatedAnn == null) {
			validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
		}
		return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
	}
}


使用细节

1、约束注解(如@NotNull)不能放在实现类上

一般情况下,我们对于Service层验证(Controller层一般都不给接口),大都是面向接口编程和使用,那么这种@NotNull放置的位置应该怎么放置呢?

看这个例子:

public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

@Validated(Default.class)
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public Object hello(Integer id, String name) {
        return null;
    }
}

约束条件都写在实现类上,按照我们所谓的经验,应该是不成问题的。但运行:

javax.validation.ConstraintDeclarationException:
A method overriding another method must not redefine the parameter constraint configuration, 
but method HelloServiceImpl#hello(Integer) redefines the configuration of HelloService#hello(Integer).

请务必注意这个异常是javax.validation.ConstraintDeclarationException,而不是错误校验错误异常javax.validation.ConstraintViolationException。请在做全局异常捕获的时候一定要区分开来~

异常信息是说parameter constraint configuration在校验方法入参的约束时,若是@Override父类/接口的方法,那么这个入参约束只能写在父类/接口上面~~~

至于为什么只能写在接口处,这个具体原因其实是和Bean Validation的实现产品有关的,比如使用的Hibernate校验,原因可参考它的此类:OverridingMethodMustNotAlterParameterConstraints

还需注意一点:若实现类写的约束和接口一模一样,那也是没问题的。比如上面若实现类这么写是没有问题能够完成正常校验的:

    @Override
    public Object hello(@NotNull @Min(10) Integer id, @NotNull String name) {
        return null;
    }

虽然能正常work完成校验,但需要深刻理解一模一样这四个字。简单的说把10改成9都会报ConstraintDeclarationException异常,更别谈移除某个注解了(不管多少字段多少注解,但凡只要写了一个就必须保证一模一样)。


关于@Override方法校验返回值方面:即使写在实现类里也不会抛ConstraintDeclarationException

另外@Validated注解它写在实现类/接口上均可~

最后你应该自己领悟到:若入参校验失败了,方法体是不会执行的。但倘若是返回值校验执行了(即使是失败了),方法体也肯定被执行了~~


2.@NotEmpty/@NotBlank只能哪些类型上?

提出这个细节的目的是:约束注解并不是能用在所有类型上的。比如若你把@NotEmpty让它去验证Object类型,它会报错如下:

javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Object'. Check configuration for 'hello.<return value>'

需要强调的是:若标注在方法上是验证返回值的,这个时候方法体是已经执行了的,这个和ConstraintDeclarationException不一样~

对这两个注解依照官方文档做如下简要说明。@NotEmpty只能标注在如下类型

  1. CharSequence
  2. Collection
  3. Map
  4. Array

注意:"“它是空的,但是” "就不是了

@NotBlank只能使用在CharSequence上,它是Bean Validation 2.0新增的注解~


3、接口和实现类上都有注解,以谁为准?

这个问题有个隐含条件:只有校验方法返回值时才有这种可能性。

public interface HelloService {
    @NotEmpty String hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

@Slf4j
@Service
@Validated(Default.class)
public class HelloServiceImpl implements HelloService {
    @Override
    public @NotNull String hello(Integer id, String name) {
        return "";
    }
}

运行案例,helloService.hello(18, “fsx”);打印如下:

javax.validation.ConstraintViolationException: hello.<return value>: 不能为空
...

到这里,可能有小伙伴就会早早下结论:当同时存在时,以接口的约束为准。

那么,我只把返回值稍稍修改,你再看一下呢???

    @Override
    public @NotNull String hello(Integer id, String name) {
        return null; // 返回值改为null
    }

再运行:

javax.validation.ConstraintViolationException: hello.<return value>: 不能为空, hello.<return value>: 不能为null
...

透过打印的信息,结论就自然不必我多。但是有个道理此处可说明:大胆猜测,小心求证


4.如何校验级联属性?

在实际开发中,其实大多数情况下我们方法入参是个对象(甚至对象里面有对象),而不是单单平铺的参数,因此就介绍一个级联属性校验的例子:

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Positive
    private Integer age;

    @Valid // 让InnerChild的属性也参与校验
    @NotNull
    private InnerChild child;

    @Getter
    @Setter
    @ToString
    public static class InnerChild {
        @NotNull
        private String name;
        @NotNull
        @Positive
        private Integer age;
    }

}

public interface HelloService {
    String cascade(@NotNull @Valid Person father, @NotNull Person mother);
}

@Slf4j
@Service
@Validated(Default.class)
public class HelloServiceImpl implements HelloService {
    @Override
    public String cascade(Person father, Person mother) {
        return "hello cascade...";
    }
}

运行测试用例:

    @Test
    public void test1() {
        helloService.cascade(null, null);
    }

输出如下:

cascade.father: 不能为null, cascade.mother: 不能为null

此处说明一点:若你father前面没加@NotNull,那打印的消息只有:cascade.mother: 不能为null

我把测试用例改造如下,你继续感受一把:

    @Test
    public void test1() {
        Person father = new Person();
        father.setName("fsx");
        Person.InnerChild innerChild = new Person.InnerChild();
        innerChild.setAge(-1);
        father.setChild(innerChild);

        helloService.cascade(father, new Person());
    }

错误消息如下(请小伙伴仔细观察和分析缘由):

cascade.father.age: 不能为null, cascade.father.child.name: 不能为null, cascade.father.child.age: 必须是正数

思考:为何mother的相关属性以及子属性为何全都没有校验呢?

  • @Valid注解

小结

本文主要讲解了@Validated的基本使用,但是注意都是在非controller层的使用。

我们知道Controller层在进行数据绑定,即DataBinder过程中是会涉及到数据校验的,那么在Controller层的方法中使用@Validated的作用是什么呢? 和@Valid注解的区别又在何处呢?


参考

Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作【享学Spring】