Constraint可以应用在带泛型的容器:Map、List、Optional,以及不带泛型的OptionalInt。
验证生效的要求
- 验证注解必须被注解在属性、方法、字段、getter(getXX,isXX)方法上
- static字段、方法无法验证
- 可以应用在接口、父类上
- 验证的目标:type、field(字段)、properties(get方法)、method-parameter、method-return-value、constructor-parameter、constructor-return-value、cross-parameter、container
- 不要重复验证field、properties
- 对象遍历、级联验证使用@Valid
- List、Map、Set、Collection、array、iterator 等容器,都可以使用@Valid注解实现验证 示例:
java
代码解读
复制代码
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。如下:
java
代码解读
复制代码
/** * Validation group verifing that a user is billable */ public interface Billable {} /** * Customer can buy without any harrassing checking process */ public interface BuyInOneClick { }
一个Constraint可以定义多个Group,如下:
java
代码解读
复制代码
/** * 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可以继承。
java
代码解读
复制代码
/** * Customer can buy without harrassing checking process */ public interface BuyInOneClick extends Default, Billable {}
Group 顺序
默认情况下,在执行校验的时候多个Group是没有特定顺序的。 Group不能形成循环依赖,否则会抛出异常GroupDefinitionException 有些情况下,需要指定Group的执行顺序。 在需要验证Bean中通过使用GroupSequence指定Group的顺序。
java
代码解读
复制代码
@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同时指定当前类、对应的分组类实现自定义默认分组
java
代码解读
复制代码
@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,那么子类默认使用父类的分组。
java
代码解读
复制代码
/** * 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进行声明
java
代码解读
复制代码
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。
java
代码解读
复制代码
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 转换也可以用于在容器的元素
java
代码解读
复制代码
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、方法和构造函数的入参、方法方式。
java
代码解读
复制代码
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)上。
java
代码解读
复制代码
public class NonNullList<@NotNull T> { } public class ContainerFactory { <@NotNull T> Container<T> instantiateContainer(T wrapped) { [...] } } public class NonNullSet<T> extends Set<@NotNull T> { }
有些不带类型实参的容器可以隐式获取类型:
java
代码解读
复制代码
@Min(1) private OptionalInt optionalNumber; @Negative private LongProperty negativeLong; @Positive private IntegerProperty positiveInt; private final ListProperty<@NotBlank StringProperty> notBlankStrings;
方法、构造函数校验
方法、构造函数校验可以直接添加Constraint注解到方法、构造函数上、对应的的参数上。 对于方法而言,可以校验请求参数,返回值,而对于构造函数而言,可以校验构造函数的入参。 方法不能为static、final,需要为public的。一般通过动态代理实现或者类似的。
验证参数
以下为参数校验:
java
代码解读
复制代码
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
java
代码解读
复制代码
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。
java
代码解读
复制代码
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,那么不会进行校验。
java
代码解读
复制代码
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) { } }
继承分层校验
有些时候需要对抽象方法进行校验(重写继承类、实现接口的方法),形成一个约定,此时可以对参数、返回值进行校验。 这种情况下有有一些规则需要满足:
- 基类可以在方法的入参、返回值增加Constraint校验标注
- 派生类在重写方法的时候,方法的入参不能再次设置Constraint校验注解(派生类的入参校验必须宽松于基类),方法的返回值可以增加校验(派生类的返回值校验必须严格于基类) 以下是错误的方式。派生类在入参进行了校验:
java
代码解读
复制代码
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) { } }
以下是正确的方式:
java
代码解读
复制代码
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) { } }