参数校验通常是 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 工厂是否已经包含了 LocalValidatorFactoryBean 和 Validator 对象, 不影响大局. 即使没有配置, 下述的代码也是会注册的
- @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 {
- // 1校验参数
- result = execVal.validateParameters(
- invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
- }
- catch (IllegalArgumentException ex) {
- // 2校验对应的桥接方法 (兼容 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);
- }
- // 3校验对应的返回值
- 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);
- 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 的校验器便完成了相应的检测功能. 逻辑还是很简单的, 希望对大家有所帮助
来源: https://www.cnblogs.com/question-sky/p/9984860.html