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注解实现验证 示例:
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) { }
}
继承分层校验
有些时候需要对抽象方法进行校验(重写继承类、实现接口的方法),形成一个约定,此时可以对参数、返回值进行校验。 这种情况下有有一些规则需要满足:
- 基类可以在方法的入参、返回值增加Constraint校验标注
- 派生类在重写方法的时候,方法的入参不能再次设置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) {
}
}