Jsr深度探险之自定义校验注解

297 阅读6分钟

Constraint

Constraint基本定义

  1. 标注@Constraint 注解,同时指定对应的ConstraintValidator,可以配置多个,但是每个ConstraintValidator需要各自处理一种类型,不能有交集
  2. 定义基础message属性: String message() default "{com.acme.constraint.MyConstraint.message}";
  3. 定义基础group属性: Class<?>[] groups() default { };
  4. 定义基础payload属性(额外元信息): Class<? extends Payload>[] payload() default { };
  5. 定义可选validationAppliesTo属性(泛型、数组类型需要): ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;设置不正确会ConstraintDeclarationException
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface OrderNumber {

    String message() default "{com.acme.constraint.OrderNumber.message}";

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

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

同一个类型使用多个相同的Constraint

  1. 定义一个List注解,有个value属性返回 自定义Constraint列表
  2. 自定义Constraint需要标注@Repeatable(List.class)
/**
 * Validate a zip code for a given country
 * The only supported type is String
 */
@Documented
@Constraint(validatedBy = ZipCodeValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface ZipCode {

    String countryCode();

    String message() default "{com.acme.constraint.ZipCode.message}";

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

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

    /**
     * Defines several @ZipCode annotations on the same element
     * @see (@link ZipCode}
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        ZipCode[] value();
    }
}
public class Address {
    @ZipCode(countryCode = "fr", groups = Default.class, message = "zip code is not valid")
    @ZipCode(
            countryCode = "fr",
            groups = SuperUser.class,
            message = "zip code invalid. Requires overriding before saving."
    )
    private String zipCode;
}
public class Address {
    @ZipCode.List( {
            @ZipCode(countryCode="fr", groups=Default.class,
                    message = "zip code is not valid"),
            @ZipCode(countryCode="fr", groups=SuperUser.class,
                    message = "zip code invalid. Requires overriding before saving.")
    } )
    private String zipCode;
}

组合Constraint

@Pattern(regexp = "[0-9]*")
@Size
@Constraint(validatedBy = FrenchZipCodeValidator.class)
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {

    String message() default "Wrong zip code";

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

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

    @OverridesAttribute(constraint = Size.class, name = "min")
    @OverridesAttribute(constraint = Size.class, name = "max")
    int size() default 5;

    @OverridesAttribute(constraint = Size.class, name = "message")
    String sizeMessage() default "{com.acme.constraint.FrenchZipCode.zipCode.size}";

    @OverridesAttribute(constraint = Pattern.class, name = "message")
    String numberMessage() default "{com.acme.constraint.FrenchZipCode.number.size}";

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        FrenchZipCode[] value();
    }
}

自定义注解的group、payload、validationAppliesTo属性会被忽略

自定义Validator

基本概念

实现ConstraintValidator接口,实现isValid方法(会并发调用isValid,需要保证线程安全)

// A 自定义的注解,T 需要校验的类型
public interface ConstraintValidator<A extends Annotation, T> {
    // 校验前的准备工作
    default void initialize(A constraintAnnotation) {
    }
    boolean isValid(T value, ConstraintValidatorContext context);
}

注意:

  1. T 没有泛型参数,或者泛型参数是具体的类型而不是 <?> 这种
  2. Validator 可以校验参数列表、跨参数校验,但是需要自定义Validator标注SupportedValidationTarget注解指定
@Documented
@Target({ TYPE })
@Retention(RUNTIME)
public @interface SupportedValidationTarget {

    ValidationTarget[] value();
}
public enum ValidationTarget {
    // 返回值
    ANNOTATED_ELEMENT,
    // 入参、构造器、跨参数
    PARAMETERS
}
  1. Validator可以同时处理单个标注的类型以及参数列表(跨参数)。如果处理多个参数需要使用Object[] 正确的定义:
//String is not making use of generics
public class SizeValidatorForString implements ConstraintValidator<Size, String> {
}

//Collection uses generics but the raw type is used
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection> {
}

//Collection uses generics and unbounded wildcard type
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection<?>> {
}

//Validator for cross-parameter constraint
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DateParametersConsistentValidator
        implements ConstraintValidator<DateParametersConsistent, Object[]> {
}

//Validator for both annotated elements and executable parameters
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS})
public class ELScriptValidator implements ConstraintValidator<ELScript, Object> {
}

错误的定义:

//parameterized type
public class SizeValidatorForString implements ConstraintValidator<Size, Collection<String>> {
}

//parameterized type using bounded wildcard
public class SizeValidatorForCollection implements ConstraintValidator<Size, Collection<? extends Address>> {
}

//cross-parameter validator accepting the wrong type
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class NumberPositiveValidator implements ConstraintValidator<NumberPositive, Number> {
}
  1. Validator可以被缓存,通过ConstraintValidatorFactory获取实例
  2. initialize只会调用一次,isValid每次校验都会调用
  3. 在initialize、isValid产生的异常会以ValidationException抛出
  4. 在isValid里不能更改传入的值

ConstraintValidatorContext

ConstraintValidatorContext提供了验证时需要的上下文。 当验证到无效时将产生一个ConstraintViolation对象,可以用于展示异常的信息。 如果验证失败了,返回抛出ValidationException。

public interface ConstraintValidatorContext {
    /**
     * 禁用默认的异常message模板
     */
    void disableDefaultConstraintViolation();
    /**
     * 获取默认未填充的message模板
     */
    String getDefaultConstraintMessageTemplate();
    /**
     * 获取当前的时间。为Past、Future constraint作为参考
     */
    ClockProvider getClockProvider();
    /**
     * 返回一个builder用于构建错误的信息
     * <pre >
     *     context.buildConstraintViolationWithTemplate( "this detail is wrong" )
     *             .addConstraintViolation();
     * </pre>
     * 在构建错误信息时,必须先调用builder的addConstraintViolation,否则抛出IllegalStateException
     * @param messageTemplate 自定义未填充消息模版
     * @return builder
     */
    ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
    /**
     *  返回provider允许访问的实例
     * @param type 实例类型
     * @return 实例
     * @param <T> 类型
     */
    <T> T unwrap(Class<T> type);
    interface ConstraintViolationBuilder {
        /**
         *  构建一个节点到validate关联的path上
         * @param name 节点名称,不能为“.”
         * @return node
         */
        NodeBuilderDefinedContext addNode(String name);
        /**
         *  构建一个属性节点到validate关联的path上
         * @param name 节点名称
         * @return node
         */
        NodeBuilderCustomizableContext addPropertyNode(String name);
        /**
         * 添加一个类级别的bean 节点到path上。bean node 为叶子结点
         * @return bean node
         */
        LeafNodeBuilderCustomizableContext addBeanNode();
        /**
         * 添加一个元素节点到path
         * @param name 名称
         * @param containerType 类型
         * @param typeArgumentIndex 类型参数索引
         * @return element node
         */
        ContainerElementNodeBuilderCustomizableContext addContainerElementNode(String name,
                                                                               Class<?> containerType, Integer typeArgumentIndex);
        /**
         *  添加一个方法参数到path上,一般用在跨参数constraint
         * @param index 参数所有
         * @return parameter node
         */
        NodeBuilderDefinedContext addParameterNode(int index);
        /**
         * 用于创建一个新的ConstraintViolation
         * @return ConstraintViolation
         */
        ConstraintValidatorContext addConstraintViolation();
    }
}

Example

简单(验证字符串是否以xxx开头)

通过使用initialize预先获取规则,然后实时调用isValid进行校验

/**
 * Check that a String begins with one of the given prefixes.
 */
public class BeginsWithValidator implements ConstraintValidator<BeginsWith, String> {

    private Set<String> allowedPrefixes;

    /**
     * Configure the constraint validator based on the elements specified at the time it was
     * defined.
     *
     * @param constraint the constraint definition
     */
    @Override
    public void initialize(BeginsWith constraint) {
        allowedPrefixes = Arrays.stream( constraint.value() )
                .collect( collectingAndThen( toSet(), Collections::unmodifiableSet ) );
    }

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition.
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null )
            return true;

        return allowedPrefixes.stream()
                .anyMatch( value::startsWith );
    }
}
跨参数验(直接验证方法的多个参数)

通过SupportedValidationTarget指定待验证的目标为方法参数,同时值的类型使用Object[]来接收多个不同类型的参数。 在isValid进行验证

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DateParametersConsistentValidator implements
        ConstraintValidator<DateParametersConsistent, Object[]> {

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition
     */
    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if ( value.length != 3 ) {
            throw new IllegalArgumentException( "Unexpected method signature" );
        }
        // one or both limits are unbounded => always consistent
        if ( value[1] == null || value[2] == null ) {
            return true;
        }
        return ( (Date) value[1] ).before( (Date) value[2] );
    }
}
泛型+跨参数

通过SupportedValidationTarget指定待验证的目标为泛型、参数。 使用Object来接收参数。

@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT, ValidationTarget.PARAMETERS})
public class ELScriptValidator implements ConstraintValidator<ELScript, Object> {

    public void initialize(ELScript constraint) {
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
    }
}
使用ConstraintValidatorContext
  1. 在initialize里获取注解的配置
  2. 在isValid验证的时候首先禁用默认的异常信息,然后在验证失败的时候构建新的错误信息 buildConstraintViolationWithTemplate + addConstraintViolation
  3. 返回false,抛出ValidateException并返回异常信息
public class SerialNumberValidator implements ConstraintValidator<SerialNumber, String> {

    private int length;

    /**
     * Configure the constraint validator based on the elements specified at the time it was
     * defined.
     *
     * @param constraint the constraint definition
     */
    @Override
    public void initialize(SerialNumber constraint) {
        this.length = constraint.length();
    }

    /**
     * Validate a specified value. returns false if the specified value does not conform to
     * the definition.
     */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if ( value == null )
            return true;

        context.disableDefaultConstraintViolation();

        if ( !value.startsWith( "SN-" ) ) {
            String wrongPrefix = "{com.acme.constraint.SerialNumber.wrongprefix}";
            context.buildConstraintViolationWithTemplate( wrongPrefix )
                    .addConstraintViolation();
            return false;
        }
        if ( value.length() != length ) {
            String wrongLength = "{com.acme.constraint.SerialNumber.wronglength}";
            context.buildConstraintViolationWithTemplate( wrongLength )
                    .addConstraintViolation();
            return false;
        }
        return true;
    }
}
基于ClockProvider验证时间

通过ClockProvider获取当前时间进行验证 传入的参数

public class PastValidatorForZonedDateTime implements ConstraintValidator<Past, ZonedDateTime> {

    @Override
    public boolean isValid(ZonedDateTime value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }

        ZonedDateTime now = ZonedDateTime.now( context.getClockProvider().getClock() );

        return value.isBefore( now );
    }
}

ConstraintValidatorFactory

用于创建ConstraintValidator。 所有被获取的ConstraintValidator都必须被release掉。 ConstraintValidatorFactory中不能缓存实例。

public interface ConstraintValidatorFactory {

    /**
     * @param key The class of the constraint validator to instantiate
     * @param <T> The type of the constraint validator to instantiate
     *
     * @return A new constraint validator instance of the specified class
     */
    <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key);

    /**
     * Signals {@code ConstraintValidatorFactory} that the instance is no longer
     * being used by the Jakarta Bean Validation provider.
     *
     * @param instance validator being released
     *
     * @since 1.1
     */
    void releaseInstance(ConstraintValidator<?, ?> instance);
}