@NotBlank注解引发的思考

203 阅读3分钟

引言

对于Java程序员来说,注解@NotBlank应该再熟悉不过了,其用于判断字符串是否为空”, 这个注解的使用需要通过maven引入hibernate相关jar包

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.8.Final</version>
</dependency>

打开@NotBlank注解源码可以得知,其可以加在方法、属性、构造器...上面,当传入的字符串为空时,@NotBlank注解就能提示我们配置好的message信息。@NotBlank注解的工作原理是啥,为啥短短一行代码,就能实现我们需要的功能,接下来本文将深入@NotBlank注解的底层,了解其是如何工作的。

@NotBalank注解和解析源码

Java程序员都知道实现自定义的注解,我们需要做两件事。第一件事,定义注解,第二件事,解析注解。以@NotBlank注解为例,第一件事,我们定义了一个名为@NotBlank的注解,如下源码:

@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();
   }
}

接下来第二件事,我们需要解析该注解,一般来说,我们需要实现ConstraintValidator接口,@NotBlank的解析类为NotBlankValidator,其实现了ConstraintValidator接口,如下源码:

public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {

    /**
     * Checks that the character sequence is not {@code null} nor empty after removing any leading or trailing
     * whitespace.
     *
     * @param charSequence the character sequence to validate
     * @param constraintValidatorContext context in which the constraint is evaluated
     * @return returns {@code true} if the string is not {@code null} and the length of the trimmed
     * {@code charSequence} is strictly superior to 0, {@code false} otherwise
     */
    @Override
    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
       if ( charSequence == null ) {
          return false;
       }

       return charSequence.toString().trim().length() > 0;
    }
}

代码调试

我们将断点打在NotBlankValidator类的某行代码上,然后发送下图所示的请求, image.png 这样我们就可以清楚地知道代码执行逻辑。红色箭头表示为重要的核心方法,蓝色箭头表示调用过程。 image.png 发起的Http请求会被拦截,从而会执行到RequestResponseBodyMethodProcessor类中的【resolveArgument】方法,最终会执行到NotBlankValidator类的【isValid】方法。相信读到这里大家已经知道了@NotBlank注解的原理。

读到这里,相信读者有很多疑问,例如:

  • 疑问一:RequestResponseBodyMethodProcessor类是如何加载到容器中的?
  • 疑问二:程序为啥会选择NotBlankValidator类去解析@NotBlank注解,而不是选择其他的解析器?

首先来解决下疑问二,在SimpleConstraintTree类中,如下所示代码行获取到@NotBlank的注解解析器NotBlankValidator

ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );

【getInitializedConstraintValidator】方法使用双重检查锁机制来获取注解解析器,如果缓存constraintValidatorCache中存在这个注解解析器,则直接返回,其中缓存key为:,否则最终会调用如下方法获取

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 );
}

。 接下来,我将阐述RequestResponseBodyMethodProcessor类是如何被加载到容器中,以及程序为啥会选择NotBlankValidator解析类去解析,

附录

在SpringMvc中,RequestResponseBodyMethodProcessor类会对http请求中的RequestBody和ResponseBody后处理。该类解析传入的参数核心方法截图如下: image.png