通过自定义一个 validator,一次搞懂参数校验的全流程流程

在web应用服务开发中,作为后端服务,需要处理各种各样的前端请求,在请求进行业务处理前,需要对请求中的参数进行合法性验证,很常用的一些验证,如:某个字段不能为空,字段长度不能超过某个阈值,邮箱字段必须符合邮箱格式等。

这些验证工作,都是一些通用的与业务关联不是很大的操作,在bean validation 中,也提供了一些通用的验证api,如如@NotNull、@NotBlank、@Min、@Max等,可以很方便地验证数据的有效性,相信很多老铁都有使用过。

使用这些注解可以简化参数校验逻辑的编写,将参数校验的逻辑集中在了具体的业务Bean上,而没有散落在controller,service中,让参数校验逻辑更加方便维护。同时,将参数校验逻辑与具体的业务进行解耦,使业务主流程逻辑更加清晰。

但是老铁们都知道这些与业务无关的参数校验通常是很简单的,复杂的参数校验逻辑往往是与业务关联比较密切的,需要依赖业务的上下文进行。对于这里验证的实现,很多老铁的解决方案又走了历史的老路:将校验逻辑分散到了各个controller和service中。真的是:把简单的留给框架,把复杂的留给自己,程序员版的买椟怀珠。

本篇文章将介绍如何自定义一个Validator,实现对具体业务场景的参数校验,同时会讲解一些,我们自定义的Validator是如何被bean validation检测并生效的。

自定义参数校验器

自定义个一个validator需要一下4个步骤。

1.定义自定义注解

我们需要定义一个自定义注解来标记需要进行验证的字段。注解可以包含自定义的属性,用于指定验证规则的细节。例如,我们可以定义一个名为@CustomValidator的注解,并在其中定义一些属性,如错误消息、验证分组等。

@Documented
@Constraint(validatedBy = CustomValidatorImpl.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValidator {
    String message() default "Invalid value";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

2.实现Validator接口

我们需要实现javax.validation.Validator接口,该接口定义了验证器的基本方法。我们可以创建一个类,例如CustomValidatorImpl,并实现接口中的validate方法。在该方法中,我们可以编写自定义的验证逻辑,对传入的值进行验证。

public class CustomValidatorImpl implements ConstraintValidator<CustomValidator, String> {
    @Override
    public void initialize(CustomValidator constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 自定义验证逻辑
        // 在这里编写验证逻辑,根据需求判断字段是否有效
        // 返回 true 表示字段有效,返回 false 表示字段无效
        return value != null && value.startsWith("custom");
    }
}

在实现ConstraintValidator 接口时,需要定义两个泛型,这两个泛型决定了这个validator适用的场景,很多时候,我们自定义的validator没有生效,可能是这两个参数配置有问题。

在 CustomValidatorImpl 中,分别对应:CustomValidatorString

CustomValidator:标识该validator和哪个注解搭配使用。

String:标识了该validator可以校验什么类型的数据。

方法 isValid 是执行参数校验逻辑的关键方法,通过该方法的返回值,来决定校验是否通过。

3.在实体类中应用自定义Validator

在需要进行验证的实体类中,我们可以使用自定义注解来标记需要验证的字段。通过在字段上添加@CustomValidator注解,并设置相应的属性,我们可以将自定义验证规则应用于该字段。

a.先定义一个并使用上述validator对其内的字段进行校验的bean

public class User {

    @CustomValidator(message = "Invalid value")
    private String customField;

    // 省略 getter 和 setter 方法
}

b.使用注解 @Valid 触发业务请求中的参数校验

@RestController
public class UserController {

    @PostMapping("/users")
    public String createUser(@Valid @RequestBody User user) {
        // 处理用户创建逻辑
        // 当请求到达该方法时,会自动触发验证过程
        // 如果验证失败,会抛出 MethodArgumentNotValidException 异常

        return "User created successfully";
    }
}

4.错误处理

如果验证失败,Spring Boot会自动处理错误信息。可以通过编写全局异常处理器或使用Spring Boot提供的默认异常处理机制来捕获和处理验证错误。这样,我们可以向用户返回友好的错误响应,指示哪些字段未通过验证。

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
        return e.getBindingResult().getFieldError().getDefaultMessage();
    }
    }

至此,一个完整的自定义Validator就实现完毕了,感兴趣的老铁,可以直接使用上述代码,然后结合自己的参数校验场景进行调整即可。

自定义的validator是如何生效的

上面提供了一个完整的自定义Validator的步骤,不知道各位老铁,会不会有以下几个问题:

1.为何使用了 @CustomValidator注解后, CustomValidatorImpl 就会生效呢?

2.定义validator时的两个泛型参数又是如何起作用的呢?

3.MethodArgumentNotValidException异常从哪里抛出来的?

4.spring-boot-starter-validation 中实现了哪些validator

针对第一个问题,如何通过 注解 @CustomValidator 找到一个 CustomValidatorImpl?

细心的小伙伴已经发现了在定义 @CustomValidator 的时候,已经通过 @Constraint(validatedBy = CustomValidatorImpl.class) 指明了使用 CustomValidatorImpl 对 CustomValidator 进行校验。

没错,实际的查找过程的确也是基于这个配置进行的。但是你有没有发现 bean validation 中内建的一些注解,其实是没有使用 参数 validatedBy 实现与具体的Validator进行关联的,比如: @NotBlack,注解的定义如下:

@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface NotBlank {

	String message() default "{javax.validation.constraints.NotBlank.message}";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };

	/**
	 * Defines several {@code @NotBlank} constraints on the same element.
	 *
	 * @see NotBlank
	 */
	@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
	@Retention(RUNTIME)
	@Documented
	public @interface List {
		NotBlank[] value();
	}
}

那么它是如何找到实现具体参数校验的validator的呢?为了防止问题扩大,我们把这个问题记一下,先回到上面的问题,虽然自定义的注解可以通过 validatedBy 找到指定的 validator ,那么整个查询的过程是怎么样的呢?如果你能把下面的流程搞清楚的话,相信你对 bean validation的理解会更加深入。

下面我们结合具体的源码来了解一下整体的查询过程:

1.spring启动参数校验

当收到请求后,spring mvc会将 http中的参数和具体的java bean进行绑定,在参数定的过程中,会启动参数校验。
具体参考类 「AbstractMessageConverterMethodArgumentResolver」:

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 进行参数绑定过程中的一段代码,其中包含了一个比较重要的判断逻辑:

通过自定义一个 validator,一次搞懂参数校验的全流程流程_第1张图片
如果需要进行参数绑定的bean使用了 @Validated 或者使用了以"Valid"开头的注解标识时,就是进行参数校验。

通过进行一路的方法跟进,来到一个重点的方法:「ConstraintHelper」类中的 「getDefaultValidatorDescriptors」:

private <A extends Annotation> List<ConstraintValidatorDescriptor<A>> getDefaultValidatorDescriptors(Class<A> annotationType) {
		//safe cause all CV for a given annotation A are CV
		final List<ConstraintValidatorDescriptor<A>> builtInValidators = (List<ConstraintValidatorDescriptor<A>>) builtinConstraints
				.get( annotationType );

		if ( builtInValidators != null ) {
			return builtInValidators;
		}

		Class<? extends ConstraintValidator<A, ?>>[] validatedBy = (Class<? extends ConstraintValidator<A, ?>>[]) annotationType
				.getAnnotation( Constraint.class )
				.validatedBy();

		return Stream.of( validatedBy )
				.map( c -> ConstraintValidatorDescriptor.forClass( c, annotationType ) )
				.collect( Collectors.collectingAndThen( Collectors.toList(), CollectionHelper::toImmutableList ) );
	}

根据方法签名,可以得知,这个方法的作用是:根据 「annotationType」 来查询 「validator」,这个方法的实现很有意思:
通过自定义一个 validator,一次搞懂参数校验的全流程流程_第2张图片

1.根据注解 找到 内建的的validator,这个通常针对 内建的注解,也就是我们常用的 @NotBlack,@NotEmpty等,刚好也回答了上面的问题:对于内建的 校验注解,虽然没有指定的 validatedBy 去可以找到一个可以使用的 validator。

2.如果找到了 自建的Validator,方法就直接返回了。其实这也表明了,我们无法对 内建的注解支持的validator进行扩展

3.对于自定的 验证注解,通过 validateBy来获取 validator,这个也就回答了上面的第一个问题。同时也回答了上面第二个问题中 注解泛型的作用。


对于第二个问题,我们只需要重点来看第二个泛型参数是如何起作用的即可。

在回答这个问题前,大家有没有发现这样一个问题:我们在使用 @NotBlack 或者 @NotEmpty的时候,这个注解可以用在多种不同类型的字段上,可以是String,List等一些我们类型上,但用在不同的字段类型的校验逻辑是不同的的,对于String,校验逻辑是判断字符本身是否为空,而对于List,则是判断集合中的是否有元素。

在回答第一个问题时,我们提到了内建注解,查找validator的过程,参考上面的代码,查找过程的重点是下面这一行代码:

 final List<ConstraintValidatorDescriptor<A>> builtInValidators = (List<ConstraintValidatorDescriptor<A>>) builtinConstraints
				.get( annotationType );
  

我们以 @NotEmpty为例,该注解绑定了多个Validator:

  List<Class<? extends ConstraintValidator<NotEmpty, ?>>> notEmptyValidators = new ArrayList<>( 11 );
		notEmptyValidators.add( NotEmptyValidatorForCharSequence.class );
		notEmptyValidators.add( NotEmptyValidatorForCollection.class );
		notEmptyValidators.add( NotEmptyValidatorForArray.class );
		notEmptyValidators.add( NotEmptyValidatorForMap.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfBoolean.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfByte.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfChar.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfDouble.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfFloat.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfInt.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfLong.class );
		notEmptyValidators.add( NotEmptyValidatorForArraysOfShort.class );
		putConstraints( tmpConstraints, NotEmpty.class, notEmptyValidators );
  

我们常用的数据类型都在其中的,那么这么多 validator 该用哪个呢, 这个就是第二个泛型字段决定的了。

那么第二个泛型字段是如何起作用的呢?

老方法,看源码,重点看类 「ConstraintValidatorManager」 的 「findMatchingValidatorDescriptor」方法:

  private <A extends Annotation> ConstraintValidatorDescriptor<A> findMatchingValidatorDescriptor(ConstraintDescriptorImpl<A> descriptor, Type validatedValueType) {
		Map<Type, ConstraintValidatorDescriptor<A>> availableValidatorDescriptors = TypeHelper.getValidatorTypes(
				descriptor.getAnnotationType(),
				descriptor.getMatchingConstraintValidatorDescriptors()
		);

		List<Type> discoveredSuitableTypes = findSuitableValidatorTypes( validatedValueType, availableValidatorDescriptors.keySet() );
		resolveAssignableTypes( discoveredSuitableTypes );

		if ( discoveredSuitableTypes.size() == 0 ) {
			return null;
		}

		if ( discoveredSuitableTypes.size() > 1 ) {
			throw LOG.getMoreThanOneValidatorFoundForTypeException( validatedValueType, discoveredSuitableTypes );
		}

		Type suitableType = discoveredSuitableTypes.get( 0 );
		return availableValidatorDescriptors.get( suitableType );
	}

在这个方法中,第一个参数是 「参数验证注解绑定的validator集合的封装」,第二个参数为具体的「被校验字段的类型」。根据字段类型从候选的validator中找到一个合适的校验器完成对参数的校验。

spring-boot-starter-validation 提供了哪些好用的validator

上文中我们知道了,validation api中提供了很多让我们可以开箱即用的注解,可以满足我们对通用字段验证的需求,那么在现有的框架中,一共支持哪些可以直接使用的validator呢?

我们以 hibernate-validator 对 validation api实现为例(我们使用的 spring-boot-starter-validation 就是基于 hibernate-validator 实现的。这里多说一点,JSR-303是JAVA EE6中的一项子规范,validation-api是一套标准(JSR-303),叫做Bean Validation,Hibernate Validator是Bean Validation的参考实现,提供了JSR-303 规范中所有内置constraint的实现)

「hibernate」对bean validation的实现主要集中在类 「ConstraintHelper」的构造方法中,感兴趣的老铁可以参考一下。

比如我们常用的判断字符串不为空的validator实现如下:

public class NotEmptyValidatorForCharSequence implements ConstraintValidator<NotEmpty, CharSequence> {

	@Override
	public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
		if ( charSequence == null ) {
			return false;
		}
		return charSequence.length() > 0;
	}
}

验证集合不为空的 validator 实现如下:

public class NotEmptyValidatorForCollection implements ConstraintValidator<NotEmpty, Collection> {

	@Override
	public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
		if ( collection == null ) {
			return false;
		}
		return collection.size() > 0;
	}
}

参数校验不通过如何处理

使用validator进行参数验证后,如果验证不通过,也就是意味着 spring mvc的参数绑定操作是存在错误的,此时就会抛出 MethodArgumentNotValidException 异常,具体看代码如下:

@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		parameter = parameter.nestedIfOptional();
		Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
		String name = Conventions.getVariableNameForParameter(parameter);

		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
			if (arg != null) {
				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);
	}

此时我们可以通过自定义一个全局的异常处理器,完成对此类异常的捕获处理即可,具体如下:

@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
        xxx
    }
}

总结

看完以上内容,希望你能够加深你对 bean validation的理解,可以在工作用起来,通过自定义validator来优化参数校验逻辑,是代码逻辑更加的清晰。如果改文章对你有帮助,欢迎点赞分享。

你可能感兴趣的:(日常随笔,Bean,Validator,参数校验,自定义Validator,NotEmpty)