这是关于Bean Validation的一系列博客的第二部分。要了解一般介绍,请先阅读此条目。本部分重点介绍约束定义。

尽管Bean Validation会附带一组预定义的基本约束(如@NotNull, @Length等等),但该规范的关键特性是其可扩展性。应用开发者可以,并且强烈鼓励编写符合特定业务需求的自定义约束。

编写自定义约束

由于编写自定义约束是规范目标的核心部分,因此已尽最大努力使过程尽可能简单。让我们来看看创建自定义约束的过程。

正如我们在之前的博客条目中看到的,约束由

  • 一个注解
  • 一个实现

约束注解

每个约束都与一个注解相关联。您可以将它视为一种类型安全的别名和描述符。约束注解还可以包含一个或多个参数,这些参数将帮助在声明时定制行为

public class Order {
    @NotNull @OrderNumber private String number;
    @Range(min=0) private BigDecimal totalPrice;
    ...
}

让我们看看@OrderNumber注解定义

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@ConstraintValidator(OrderNumberValidator.class)
public @interface OrderNumber {
    String message() default "{error.orderNumber}"; 
    String[] groups() default {}; 
}

约束注解只是带有一些额外功能的常规注解

  • 它们必须使用运行时保留策略:Bean Validation提供程序将在运行时检查您的对象
  • 它们必须被注解为@ConstraintValidator
  • 它们必须有一个消息属性
  • 它们必须有一个groups属性

@ConstraintValidator属性指示Bean Validation提供程序该注解是约束注解。它还指向约束验证实现例程(我们将在下一部分描述它)。

消息属性(通常默认为一个键)提供了在约束错误列表中覆盖默认消息的能力。我们将在后面的文章中介绍这个特定的主题。

groups允许约束声明定义其参与的约束子集。组可以启用部分验证和有序验证。我们将在后续文章中介绍这个特定主题。

除了这些必填属性外,注解还可以定义任何附加元素来参数化约束逻辑。参数集传递给约束实现。例如,一个@Range注解需要minmax属性。

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@ConstraintValidator(RangeValidator.class)
public @interface Range {
        long max() default Long.MAX_VALUE;

        long min() default Long.MIN_VALUE;

        String message() default "{error.range}";
        String[] groups() default {}; 
}

现在我们已经有了一种表达约束及其参数的方法,我们需要提供验证约束的逻辑。

约束实现

约束实现通过使用@ConstraintValidator与其注解相关联。在第一份早期草案中,@ValidatorClass有时被用于代替@ConstraintValidator:这是由于最后一刻的改变导致的错误,抱歉。实现必须实现一个非常简单的接口Constraint<A extends Annotation>其中A是目标约束注解

public class OrderNumberValidator implements Constraint<OrderNumber> {
        public void initialize(OrderNumber constraintAnnotation) {
                //no initialization needed
        }

        /**
         * Order number are of the form Nnnn-nnn-nnn when n is a digit
         * The sum of each nnn numbers must be a multiple of 3
         */
        public boolean isValid(Object object) {
                if ( object == null) return true;
                if ( ! (object instanceof String) )
                        throw new IllegalArgumentException("@OrderNumber only applies to String");
                String orderNumber = (String) object;
                if ( orderNumber.length() != 12 ) return false;
                if ( orderNumber.charAt( 0 ) != 'N'
                                || orderNumber.charAt( 4 ) != '-'
                                || orderNumber.charAt( 8 ) != '-'
                                ) return false;
                try {
                        long result = Integer.parseInt( orderNumber.substring( 1, 4 ) )
                                        + Integer.parseInt( orderNumber.substring( 5, 8 ) )
                                        + Integer.parseInt( orderNumber.substring( 9, 12 ) );
                        return result % 3 == 0;
                }
                catch (NumberFormatException nfe) {
                        return false;
                }
        }
}

initialize方法接收约束注解作为参数。此方法通常用于

  • isValid方法
  • 准备参数

如果需要的话,获取外部资源

isValid如您所见,该接口完全专注于验证,将其他关注点,如错误渲染,留给bean验证提供者。

  • isValid必须支持并发调用
  • 当收到的对象类型与验证实现期望不匹配时,应抛出异常
  • 空值不被视为无效:规范建议将核心约束验证与空值约束验证分开,并使用@NotNullif the property must not be null

这种易于自定义的方法为应用程序员提供了表达约束和验证它们所需的空间。

多次应用相同的约束类型

尤其是在使用groups时,您有时需要在同一元素上多次应用同一类型的约束。Bean Validation规范考虑了包含约束注解数组的注解

@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Patterns {
        Pattern[] value();
}

@ConstraintValidator(PatternValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Pattern {
        /** regular expression */
        String regex();

        /** regular expression processing flags */
        int flags() default 0;

        String message() default "{validator.pattern}";
        
        String[] groups() default {};
}

在这个例子中,您可以对同一属性应用多个模式

public class Engine {
        @Patterns( {
            @Pattern(regex = "^[A-Z0-9-]+$", message = "must contain alphabetical characters only"),
            @Pattern(regex = "^....-....-....$", message="must match ....-....-....")
                        } )
        private String serialNumber;
        ...

构建约束

默认情况下,Bean Validation提供者使用无参数构造函数实例化约束验证实现。然而,规范提供了一个扩展点,可以将实例化过程委托给依赖管理库,如Web Beans、Guice、Spring、JBoss Seam或甚至JBoss Microcontainer。

根据依赖管理工具的能力,我们期望验证实现能够在需要时接收注入的资源:此机制将完全取决于依赖管理工具。

类级约束

一些用户表示了对应用跨越多个属性的范围约束的能力或表达依赖于多个属性的约束的能力的担忧。经典的例子是地址验证。地址有复杂的规则

  • 街道名称是相当标准的,必须肯定有一个长度限制
  • 邮编结构完全取决于国家
  • 城市通常可以与邮编相关联,并进行一些错误检查(如果可以访问验证服务的话)
  • 由于这些相互依赖,简单的属性级约束不足以解决问题

Bean Validation规范提供的解决方案有两个

  • 它通过使用组和组序列,允许通过使用组和组序列强制应用一组约束在另一组约束之前。这个主题将在下一篇文章中介绍
  • 它允许定义类级别的约束

类级别约束是常规约束(注解/实现对),应用于类而不是属性。换句话说,类级别约束接收对象实例(而不是属性值)isValid.

@Address 
public class Address {
    @NotNull @Max(50) private String street1;
    @Max(50) private String street2;
    @Max(10) @NotNull private String zipCode;
    @Max(20) @NotNull String city;
    @NotNull private Country country;
    
    ...
}
@ConstraintValidator(MultiCountryAddressValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface Address {
    String message() default "{error.address}";
    String[] groups() default {};
}
public class MultiCountryAddressValidator implements Constraint<Address> {
        public void initialize(Address constraintAnnotation) {
                //initialize the zipcode/city/country correlation service
        }

        /**
         * Validate zipcode and city depending on the country
         */
        public boolean isValid(Object object) {
                if ( ! (object instanceof Address) )
                        throw new IllegalArgumentException("@Address only applies to Address");
                Address address = (Address) object;
                Country country = address.getCountry();
                if ( country.getISO2() == "FR" ) {
                    //check address.getZipCode() structure for France (5 numbers)
                    //check zipcode and city correlation (calling an external service?)
                    return isValid;
                }
                else if ( country.getISO2() == "GR" ) {
                    //check address.getZipCode() structure for Greece
                    //no zipcode / city correlation available at the moment
                    return isValid;
                }
                ...
        }
}

高级地址验证规则已被从地址对象中移除,并由MultiCountryAddressValidator实现。通过访问对象实例,类级别约束具有很大的灵活性,可以验证多个相关属性。请注意,这里忽略了排序,我们将在下一篇文章中回到这个问题。

专家小组讨论了各种支持多个属性的方法:我们认为,与涉及依赖关系的其他属性级别方法相比,类级别约束方法既提供了足够的简洁性,又提供了足够的灵活性。欢迎您提出反馈。

结论

自定义约束是JSR 303 Bean Validation灵活性的核心。编写自定义约束不应被认为是不合适的

  • 验证过程可以捕获您期望的精确验证语义
  • 精心选择的注解名称将使约束在代码中非常易于阅读

请在此处告诉我们您的看法 这里。您可以从此处下载完整的规范草案。下一篇文章将涵盖组、约束子集和验证排序。


回到顶部