Jsr深度探险之限制声明 + 验证流程

109 阅读7分钟

Constraint可以应用在带泛型的容器:Map、List、Optional,以及不带泛型的OptionalInt。

验证生效的要求

  1. 验证注解必须被注解在属性、方法、字段、getter(getXX,isXX)方法上
  2. static字段、方法无法验证
  3. 可以应用在接口、父类上
  4. 验证的目标:type、field(字段)、properties(get方法)、method-parameter、method-return-value、constructor-parameter、constructor-return-value、cross-parameter、container
  5. 不要重复验证field、properties
  6. 对象遍历、级联验证使用@Valid
  7. List、Map、Set、Collection、array、iterator 等容器,都可以使用@Valid注解实现验证 示例:
public class User {
    // preferred style as of Jakarta Bean Validation 2.0
    private List<@Valid PhoneNumber> phoneNumbers;
    // traditional style; continues to be supported
    @Valid
    private List<PhoneNumber> phoneNumbers;
    /**
     * 会重复验证
     */
    @Valid
    private List<@Valid PhoneNumber> phoneNumbers;
}

public class User {
    // preferred style as of Jakarta Bean Validation 2.0
    private Map<AddressType, @Valid Address> addressesByType;

    // traditional style; continues to be supported
    @Valid
    private Map<AddressType, Address> addressesByType;
    /**
     * 会重复验证
     */
    @Valid
    private Map<AddressType, @Valid Address> addressesByType;
}
public class User {
    private Map<@Valid AddressType, @Valid Address> addressesByType;
}
public class User {
    private Map<String, List<@Valid Address>> addressesByType;
}
public class User {
    private Map<String, Map<@Valid AddressType, @Valid Address>> addressesByUserAndType;
}

Constraint声明

Constraint的声明通过注解标注到类、接口上实现。 当Constraint标注到类上,实例会被传入到定义的ConstraintValidator进行验证。 当Constraint标注到字段上,字段的值会被传入到ConstraintValidator进行验证。 当Constraint标注到getter方法上,方法调用后的返回值会被传入到ConstraintValidator进行验证。

Group

基本

Group 是constraint的一个子集,可以只验证某个子集。 每个Constraint需要声明所在的分组,默认在Default。 Group的表现形式为定义一个interface。如下:

/**
 * Validation group verifing that a user is billable
 */
public interface Billable {}

/**
 * Customer can buy without any harrassing checking process
 */
public interface BuyInOneClick {
}

一个Constraint可以定义多个Group,如下:

/**
 * User representation
 */
public class User {
    @NotNull
    private String firstname;

    @NotNull(groups = Default.class)
    private String lastname;

    @NotNull(groups = {Billable.class, BuyInOneClick.class})
    private CreditCard defaultCreditCard;
}

继承

Group可以继承。

/**
 * Customer can buy without harrassing checking process
 */
public interface BuyInOneClick extends Default, Billable {}

Group 顺序

默认情况下,在执行校验的时候多个Group是没有特定顺序的。 Group不能形成循环依赖,否则会抛出异常GroupDefinitionException 有些情况下,需要指定Group的执行顺序。 在需要验证Bean中通过使用GroupSequence指定Group的顺序。

@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
    @NotNull @Size(max = 50)
    private String street1;

    @NotNull @ZipCode
    private String zipCode;

    @NotNull @Size(max = 30)
    private String city;

    /**
     * check coherence on the overall object
     * Needs basic checking to be green first
     */
    public interface HighLevelCoherence {}

    /**
     * check both basic constraints and high level ones.
     * high level constraints are not checked if basic constraints fail
     */
    @GroupSequence({Default.class, HighLevelCoherence.class})
    public interface Complete {}
}

使用Address.Complete标注Group顺序,在验证的时候先验证Default,再验证HighLevelCoherence。如果Default失败了不会再验证HighLevelCoherence。

重定义Default分组

有些时候需要修改默认分组来替代Default。 通过在待校验类上标注@GroupSequence同时指定当前类、对应的分组类实现自定义默认分组

@GroupSequence({Address.class, HighLevelCoherence.class})
@ZipCodeCoherenceChecker(groups = Address.HighLevelCoherence.class)
public class Address {
    @NotNull @Size(max = 50)
    private String street1;

    @NotNull @ZipCode
    private String zipCode;

    @NotNull @Size(max = 30)
    private String city;

    /**
     * check coherence on the overall object
     * Needs basic checking to be green first
     */
    public interface HighLevelCoherence {}
}

隐式分组

默认情况下,如果Constraint没有显式指定Group,那么默认使用Default分组。 如果有父类指定了Constraint,那么子类默认使用父类的分组。

/**
 * Auditable object contract
 */
public interface Auditable {
    @NotNull String getCreationDate();
    @NotNull String getLastUpdate();
    @NotNull String getLastModifier();
    @NotNull String getLastReader();
}

/**
 * Represents an order in the system
 */
public class Order implements Auditable {
    private String creationDate;
    private String lastUpdate;
    private String lastModifier;
    private String lastReader;

    private String orderNumber;

    public String getCreationDate() {
        return this.creationDate;
    }

    public String getLastUpdate() {
        return this.lastUpdate;
    }

    public String getLastModifier() {
        return this.lastModifier;
    }

    public String getLastReader() {
        return this.lastReader;
    }

    @NotNull @Size(min=10, max=10)
    public String getOrderNumber() {
        return this.orderNumber;
    }
}

Group转换

当在使用@Valid级联校验时,可能需要使用不同的分组,而不是请求时原始的分组。可以通过使用@ConvertGroup进行声明

package jakarta.validation.groups;
/**
 * Converts group {@code from} to group {@code to} during cascading.
 * <p>
 * Can be used everywhere {@link Valid} is used and must be on an element
 * annotated with {@code Valid}.
 *
 * @author Emmanuel Bernard
 * @since 1.1
 */
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
public @interface ConvertGroup {

    /**
     * The source group of this conversion.
     * @return the source group of this conversion
     */
    Class<?> from() default Default.class;

    /**
     * The target group of this conversion.
     * @return the target group of this conversion
     */
    Class<?> to();

    /**
     * Defines several {@link ConvertGroup} annotations
     * on the same element.
     */
    @Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {

        ConvertGroup[] value();
    }
}

@ConvertGroup和@ConvertGroup.List可以使用在任何使用了@Valid的地方(关联、方法、构造函数请求参数、返回值)。如果没有@Valid会抛出ConstraintDeclarationException异常。 当标注了@Valid后,验证会进行传播,级联使用对应的Group进行验证,直到遇到ConvertGroup。 如果当前Group是ConvertGroup配置的from,那么会此时使用ConvertGroup的To对应的Group进行处理。 如果ConvertGroup为指定,默认为Default Group。 当ConvertGroup匹配了后就不会继续往下匹配了。比如:1: from:A to:B 2. from:B to:C 。此时验证时,如果当前Group是A,那么匹配到了B后,就不会再次匹配了。 如果标注的多个ConvertGroup中,from相同,那么会抛出ConstraintDeclarationException异常。 ConvertGroup的from不能为group sequence,to可以。

以下示例会转换User当前的Group到Address需要的Group。 当验证User实例时,如果是Default,那么对应级联的内部的验证就会使用BasicPostal group,如果是Complete,那么会是FullPostal。

public interface Complete extends Default {}
public interface BasicPostal {}
public interface FullPostal extends BasicPostal {}

public class Address {
    @NotNull(groups=BasicPostal.class)
    String street1;

    String street2;

    @ZipCode(groups=BasicPostal.class)
    String zipCode;

    @CodeChecker(groups=FullPostal.class)
    String doorCode;
}

public class User {
    @Valid
    @ConvertGroup(from=Default.class, to=BasicPostal.class)
    @ConvertGroup(from=Complete.class, to=FullPostal.class)
    Set<Address> getAddresses() { [...] }
}

Group 转换也可以用于在容器的元素

public class User {
    Set<
        @Valid
        @ConvertGroup(from=Default.class, to=BasicPostal.class)
        @ConvertGroup(from=Complete.class, to=FullPostal.class)
        Address
    > getAddresses() { [...] }
}

验证容器元素

parameters: 形参 arguments: 实参

Constraint可以被用在泛型容器的元素上,比如:List、Map、Optional等,通过标注Constraint注解到类型实参(type arguments)上。 可以被校验的容器类型有:field、properties、方法和构造函数的入参、方法方式。

private List<@Email String> emails;

public Optional<@Email String> getEmail() {
}

public Map<@NotNull String, @ValidAddress Address> getAddressesByType() {
}

public List<@NotBlank String> getMatchingRecords(List<@NotNull @Size(max=20) String> searchTerms) {
}
private Map<String, @NotEmpty List<@ValidAddress Address>> addressesByType;

容器验证不支持标注到类型形参(type parameters)、带有extends和implements的类型实参(type arguments)上。

public class NonNullList<@NotNull T> {
}

public class ContainerFactory {
    <@NotNull T> Container<T> instantiateContainer(T wrapped) { [...] }
}

public class NonNullSet<T> extends Set<@NotNull T> {
}

有些不带类型实参的容器可以隐式获取类型:

@Min(1)
private OptionalInt optionalNumber;

@Negative
private LongProperty negativeLong;

@Positive
private IntegerProperty positiveInt;

private final ListProperty<@NotBlank StringProperty> notBlankStrings;

方法、构造函数校验

方法、构造函数校验可以直接添加Constraint注解到方法、构造函数上、对应的的参数上。 对于方法而言,可以校验请求参数,返回值,而对于构造函数而言,可以校验构造函数的入参。 方法不能为static、final,需要为public的。一般通过动态代理实现或者类似的。

验证参数

以下为参数校验:

public class OrderService {

    public OrderService(@NotNull CreditCardProcessor creditCardProcessor) {
    }

    public void placeOrder(
        @NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity) {
    }
}

以下为跨参数校验,可以校验多个参数,使用上跟类级别的校验注解一样: 需要校验startDate需要早于endDate

public class CalendarService {

    @ConsistentDateParameters
    public void createEvent(
        String title,
        @NotNull Date startDate,
        @NotNull Date endDate) {
    }
}

跨参数注解不能使用在没有参数的方法上,不然会抛出ConstraintDeclarationException异常。 在定义跨参数校验注解的时候,为了避免歧义,建议配置validationAppliesTo为ConstraintTarget.PARAMETERS。 为了在进行跨参数校验时获取参数的名称,提供了jakarta.validation.ParameterNameProvider当前正在代理的对应的参数的名称。

验证返回值

通过标注Constraint注解到方法、构造函数上实现返回值的校验。 有些Constraint可以同时校验参数、返回值。这种情况下最好建议配置validationAppliesTo到ConstraintTarget.RETURN_VALUE。

public class OrderService {

    private CreditCardProcessor creditCardProcessor;

    @ValidOnlineOrderService
    public OrderService(OnlineCreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @ValidBatchOrderService
    public OrderService(BatchCreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @NotNull
    @Size(min=1)
    public Set<CreditCardProcessor> getCreditCardProcessors() { [...] }

    @NotNull
    @Future
    public Date getNextAvailableDeliveryDate() { [...] }
}

级联校验

通过使用@Valid实现方法参数、返回值内部引用的级联校验。如果参数、返回值是null,那么不会进行校验。

public class OrderService {

    @NotNull @Valid
    private CreditCardProcessor creditCardProcessor;

    @Valid
    public OrderService(@NotNull @Valid CreditCardProcessor creditCardProcessor) {
        this.creditCardProcessor = creditCardProcessor;
    }

    @NotNull @Valid
    public Order getOrderByPk(@NotNull @Valid OrderPK orderPk) {  }

    @NotNull
    public Set<@Valid Order> getOrdersByCustomer(@NotNull @Valid CustomerPK customerPk) { }
}

继承分层校验

有些时候需要对抽象方法进行校验(重写继承类、实现接口的方法),形成一个约定,此时可以对参数、返回值进行校验。 这种情况下有有一些规则需要满足:

  1. 基类可以在方法的入参、返回值增加Constraint校验标注
  2. 派生类在重写方法的时候,方法的入参不能再次设置Constraint校验注解(派生类的入参校验必须宽松于基类),方法的返回值可以增加校验(派生类的返回值校验必须严格于基类) 以下是错误的方式。派生类在入参进行了校验:
public interface OrderService {

    void placeOrder(String customerCode, Item item, int quantity);
}

public class SimpleOrderService implements OrderService {

    @Override
    public void placeOrder(
        @NotNull @Size(min=3, max=20) String customerCode,
        @NotNull Item item,
        @Min(1) int quantity) {
    }
}

以下是正确的方式:

public class OrderService {

    Order placeOrder(String customerCode, Item item, int quantity) {
        [...]
    }
}

public class SimpleOrderService extends OrderService {

    @Override
    @NotNull
    @Valid
    public Order placeOrder(String customerCode, Item item, int quantity) {
    }
}