? JSR:全称Java Specification Requests,意思是Java 规范提案。我们可以将其理解为Java为一些功能指定的一系列统一的规范。跟数据校验相关的最新的JSR为JSR 380。
? Bean Validation 2.0 是JSR第380号标准。该标准连接如下:https://www.jcp.org/en/egc/view?id=380
? Bean Validation的主页:http://beanvalidation.org
? Bean Validation的参考实现:https://github.com/hibernate/hibernate-validator
Bean Validation 2.0的唯一实现就是Hibernate Validator,对应版本为6.0.1.Final(JDK8)
Bean Validation 3.0的唯一实现就是Hibernate Validator,对应版本为7.0.5.Final(JDK 9) ,8.0.0.Final(JDK10)
? 参考:https://download.oracle.com/otndocs/jcp/bean_validation-2_0_0-final-spec/
验证注解 | 验证的数据类型 | 说明 |
---|---|---|
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
@FutureOrPresent | ||
@Negative | ||
@NegativeOrZero | ||
@PastOrPresent | ||
@Positive | ||
@PositiveOrZero |
约束是一系列约束注解与约束校验实现的组合。约束注解可以应用在types, fields, methods,constructors, parameters, container elements 或其他约束注解。
Bean校验API的默认包名为javax.validation
。
约束通过在Java Bean上应用约束注解来表达。约束注解是应用了javax.validation.Constraint
,并且retention
策略设置为RUNTIME
的注解。
Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}
Generic 约束注解可以应用于:
Cross-parameter约束注解可以应用于:
一个Constraint注解可以同时应用于Generic 约束注解 和 Cross-parameter约束注解
在指定一个注解应用于Java Bean的同时,需要指定属性,这些属性映射为注解元素。注解元素包括:message
, groups
, validationAppliesTo
和payload
,为保留名称,注解元素命名不能以valid
开头,constraint 可以使用其他元素名称作为其属性。
每个约束注解都必须定义一个String
类型的message
元素。
String message() default "{com.acme.constraint.MyConstraint.message}";
message
元素的值用于描述错误信息。
groups用于定义约束定义关联到哪个处理Group,类型为Class<?>[]
Class<?>[] groups() default {};
默认值为空数组。如果未定义,则表示使用Default
组。
类型为 Payload[]
,默认值为空数组。
Class<? extends Payload>[] payload() default {};
public interface Payload {
}
payload通常用于定义关联到约束注解的元数据,在client进行校验时使用。
ConstraintTarget
枚举类型,用于指定注解应用于什么类型。
ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
public enum ConstraintTarget {
/**
当没有方法和构造函数时,表示type,field。当方法和构造函数没有参数时,表示返回值,当没有返回值时,表示参数。
*/
IMPLICIT,
RETURN_VALUE,
PARAMETERS
}
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface Min {
String message() default "{javax.validation.constraints.Min.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
long value();
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
Min[] value();
}
}
多个同一约束(有不同的属性)应用于同一个target。为了支持这个特性, Bean Validation提供者对value
属性返回 约束注解 数组的注解进行了特殊处理。value
属性值的每个元素都应用于目标。
借助于JDK8 的@Repeatable
注解,在每个约束注解类定义一个 List
注解,用于此类约束注解的容器。(见 @Min
的定义代码)
示例:
//定义约束注解
@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 {};
/**
* 此处定义了 ZipCode 的容器
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
ZipCode[] value();
}
}
//使用注解: 方式1.
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;
}
//使用注解:方式2,使用List。
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;
}
组合多个不同类型的约束。定义一个注解,把要应用的多个注解 应用于 此注解的定义上。
示例:
@Pattern(regexp = "[0-9]*") // 约束1
@Size(min = 5, max = 5) // 约束2
@Constraint(validatedBy = FrenchZipCodeValidator.class) //注意此处
@ReportAsSingleViolation //所有约束都产生在一个错误报告中
@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 {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
FrenchZipCode[] value();
}
}
默认情况下,每个约束都会产生一个错误报告,每个composing约束的groups
、payload
、validationAppliesTo
属性 会继承主约束(上例:FrenchZipCode)的对应属性,自己的定义会忽略。
如果需要所有约束的错误都产生在一个错误报告中,则使用注解@ReportAsSingleViolation
。当发现第一个错误时即停止后续校验。
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ReportAsSingleViolation {
}
@OverridesAttribute
使用 @OverridesAttribute
,可以在主约束中通过属性覆盖 组合约束的属性。
@Documented
@Retention(RUNTIME)
@Target({ METHOD })
@Repeatable(List.class)
public @interface OverridesAttribute {
Class<? extends Annotation> constraint();
String name() default "";
int constraintIndex() default -1;
@Documented
@Target({ METHOD })
@Retention(RUNTIME)
public @interface List {
OverridesAttribute[] value();
}
}
@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 {};
//此处 size()属性,覆盖了 @Size的min 属性
@OverridesAttribute(constraint = Size.class, name = "min")
//此处 size()属性,覆盖了 @Size的max 属性
@OverridesAttribute(constraint = Size.class, name = "max")
int size() default 5;
//此处sizeMessage()属性,覆盖了 @Size的message属性
@OverridesAttribute(constraint = Size.class, name = "message")
String sizeMessage() default "{com.acme.constraint.FrenchZipCode.zipCode.size}";
//此处numberMessage()属性,覆盖了 @Pattern的message属性
@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();
}
}
constraintIndex
属性用于指定同一个约束的index
@Documented
@Constraint(validatedBy = {})
@Pattern(regexp = "[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}") // email
@Pattern(regexp = ".*?emmanuel.*?") // emmanuel
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface EmmanuelsEmail {
String message() default "Not emmanuel's email";
//第 1 个 Pattern
@OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 0)
String emailMessage() default "Not an email";
@OverridesAttribute(constraint = Pattern.class, name = "message", constraintIndex = 1)
String emmanuelMessage() default "Not Emmanuel";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
EmmanuelsEmail[] value();
}
}
一个约束校验实现 执行一个特定约束的校验是通过约束注解的validatedBy
指定的。约束校验实现需要 实现ConstraintValidator
接口
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {
}
/** 实现校验逻辑
* @param value 待校验的对象
* @param context 校验上下文
*/
boolean isValid(T value, ConstraintValidatorContext context);
}
泛型参数T 要求:
<?>
约束校验实现的实例是由ConstraintValidatorFactory
创建的。由谁创建就由谁释放,不由创建者释放则会出错误。
public interface ConstraintValidatorFactory {
<T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key);
void releaseInstance(ConstraintValidator<?, ?> instance);
}
容器元素约束的校验需要从容器获取元素,容器元素的获取通过ValueExtractor
实现。
public interface ValueExtractor<T> {
/** 从 originalValue 获取值,不可为null。
* @param receiver 写入值。
*/
void extractValues(T originalValue, ValueReceiver receiver);
/** 获取的值保存。
*/
interface ValueReceiver {
/**
* Receives the value extracted from an object.
* @param object 待校验的value
*/
void value(String nodeName, Object object);
void iterableValue(String nodeName, Object object);
void indexedValue(String nodeName, int i, Object object);
void keyedValue(String nodeName, Object key, Object object);
}
}
用于指示一个 ValueExtractor
抽取的元素值
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE_USE)
@Documented
public @interface ExtractedValue {
/**
* @return the type of the value extracted by the value extractor
*/
Class<?> type() default void.class;
}
// 注意:@ExtractedValue
class ListValueExtractor implements ValueExtractor<List<@ExtractedValue ?>> {
@Override
public void extractValues(List<?> originalValue, ValueReceiver receiver) {
for ( int i = 0; i < originalValue.size(); i++ ) {
receiver.indexedValue( "<list element>", i, originalValue.get( i ) );
}
}
}
//
@UnwrapByDefault
public class OptionalIntValueExtractor implements ValueExtractor<@ExtractedValue(type =
Integer.class) OptionalInt> {
@Override
public void extractValues(OptionalInt originalValue, ValueReceiver receiver) {
receiver.value( null, originalValue.isPresent() ? originalValue.getAsInt() : null );
}
}
Bean Validation规范 为声明约束定义了一个框架,约束是在类型上声明的,并根据实例或实例图进行校验。
? 承载约束并希望被Bean Validation Provider 验证的对象必须满足以下要求:
要验证的属性必须遵循Java Bean 规范读取方法的签名,一般为getter。
静态字段和静态方法被排除在验证之外。
约束注解定义的目标元素能够是字段、属性或者类型等。
能够在类或者接口上使用约束验证,它将对该类或实现该接口的实例进行状态验证;
字段和属性均能够使用约束验证,可是不能将同样的约束反复声明在字段和相关属性(字段的 getter 方法)上。
容器元素(container element)约束需要
Value Extractor
协助。
约束声明可以应用于类或接口。将约束应用到类或接口表示对类或实现接口的类的状态进行校验。
约束声明可以应用于同一对象类型的字段和属性。然而,相同的约束不应该在字段及其关联属性之间重复(约束校验将应用两次)。建议持有约束声明的对象遵循单一状态访问策略(带注解的字段或属性)。
当使用约束注解声明在字段时,将使用字段访问策略访问由该约束校验的状态。
当使用约束注解声明在属性时,将使用属性访问策略访问由该约束校验的状态。
当使用字段访问策略时,Bean Validation Provider
直接访问实例变量。在使用属性访问策略时,Bean Validation Provider
通过属性访问器方法。要求该类遵循JavaBeans读属性(由JavaBeans Introspector类定义)的方法签名约定。
Java.beans.Introspector.decapitalize()方法用于把getter名称转成字段名称。
除了支持Java Bean 实例的校验外,还支持Object Graph
的校验。Object Graph
即为对象的拓扑结构,如对象之间的引用关系。
假设类 A 引用类 B,则在对类 A 的实例进行约束验证时也须要对类 B 的实例进行约束验证,这就是验证的级联性。
当对 Java 语言中的集合(Collection,List,Set,Map,Iterater)、数组等类型进行验证时也须要对该类型的每个元素进行验证。
Object Graph
的校验的结果以统一的约束冲突集合返回。@Valid
用于表示对级联的校验遍历。
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
对于一个容器,@Valid
即可以应用于容器自身上,也可以应用于容器的元素类型上。但是不要2个都应用(会一起2次校验)。
不支持把@Valid
应用于泛型类型和方法的类型参数上。也不支持应用于extends,implements 的类型主体上。
public class Person {
@NotEmpty
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Order {
/**在对 Order 的实例进行验证时,仅仅有当在 Order 引用的对象 Person 前面声明了注解 @Valid,才对 Person 中 name 字段的 @NotEmpty 注解进行验证,否则将不予验证。
*/
@Valid
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
public class User {
// Bean Validation 2.0 建议的方式
private List<@Valid PhoneNumber> phoneNumbers;
// 传统的方式
@Valid
private List<PhoneNumber> phoneNumbers;
// 不提倡 2 种方式一起用
@Valid
private List<@Valid PhoneNumber> phoneNumbers;
}
//MAP
private Map<AddressType, @Valid Address> addressesByType;
private Map<@Valid AddressType, @Valid Address> addressesByType;
//嵌套
// 2 个 ValueExtractor 被调用: Map,List。
private Map<String, List<@Valid Address>> addressesByType;
约束声明主要通过在类和接口上应用约束注解。当约束声明在类上,则类的实例被传给ConstraintValidator
,在字段或属性,则字段值或方法的返回值传给ConstraintValidator
。
约束可以声明在接口上,对于一个类,在超类上或者接口上声明的约束,将由 Bean Validation provider
评估,规则由 Group
定义。
约束声明的效果是累积的。根据Java语言规范可见性规则,在超类getter上声明的约束将与在getter的Override版本上定义的任何约束一起进行验证。
Bean Validation 规范中一个重要的概念。就是组和组序列。组定义了约束的子集。对于一个给定的 Object Graph 结构,有了组的概念。则无需对该 Object Graph 中全部的约束进行验证。仅仅须要对该组定义的一个子集进行验证就可以。
组别验证须要在约束声明时进行组别的声明。否则使用默认的组 Default.class
。组通过接口的方式进行定义。
一个约束可以属于1个或多个组。
package javax.validation.groups;
public interface Default {
}
一个组可以是1个或多个组的超集。使用extends
public interface BuyInOneClick extends Default, Billable {}
子集的校验会触发超类的校验。
public class User {
@NotNull
private String firstname;
@NotNull(groups = Default.class)
private String lastname;
@NotNull(groups = Billable.class)
private CreditCard defaultCreditCard;
}
// 校验
validator.validate(user,BuyInOneClick.class)
? 组
BuyInOneClick
的校验 会触发 组Default
,Billable
的校验。则 firstname,lastname,defaultCreditCard 都会校验@Notnull
。
默认情况下。不同组别的约束验证是无序的,然而在某些情况下,约束验证的顺序却非常重要,如以下两个样例:
第二个组中的约束验证依赖于一个稳定状态来执行。而这个稳定状态是由第一个组来进行验证的。
某个组的验证比较耗时。CPU 和内存的使用率相对照较大。最优的选择是将其放在最后进行验证。
因此。在进行组验证的时候尚需提供一种有序的验证方式。这就提出了组序列的概念。
每个组序列中的组都必须按照指定的顺序处理,顺序通过 @GroupSequence.value
进行定义。在使用组序列验证的时候,假设序列前边的组验证失败,则后面的组将不再给予验证。
定义一个序列的组或者组合一个序列的组,都不应该有循环。否则会抛出
GroupDefinitionException
。
定义一个序列的组,**不要继承其他组。**定义一个序列的组,不要直接用在约束声明中。
定义一个序列的组,要在接口上使用 @GroupSequence
注解。
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {
Class<?>[] value();
}
public interface GroupA {
}
public interface GroupB {
}
//此处定义了顺序。
@GroupSequence({Default.class, GroupA.class, GroupB.class})
public interface Group {
}
public class User {
@NotEmpty (message = "firstname may be empty")
private String firstname;
@NotEmpty(message = "middlename may be empty", groups = Default.class)
private String middlename;
@NotEmpty(message = "lastname may be empty",groups = GroupA.class)
private String lastname;
@NotEmpty(message = "country may be empty",groups = GroupB.class)
private String country;
}
public static void main(String[] args){
User user = new User();
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<User>> set = validator.validate(user,Group.class);
for (ConstraintViolation<User> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
}
默认情况下,校验Default
Group时,不会校验其他Group。为了在校验Default
组时同时校验其他Group,则可以在类上注解 @GroupSequence
,用于重新定义Default
。
//注意:此处放置 GroupSequence,则表示Default组,被修改了
@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;
public interface HighLevelCoherence {}
}
}
当类被校验时,则注解在类上的
GroupSequence
中的Group会同时被校验。
可以隐式的把多个约束组织在一个Group中,而不用显示的把Group设置在约束中。每个放置在接口Z
上的约束以及Default 组的部分都属于 Group Z
,这对于基于接口表示的角色验证对象的部分状态非常有用。
//接口方法应用了 约束。
public interface Auditable {
@NotNull String getCreationDate();
@NotNull String getLastUpdate();
}
public class Order implements Auditable {
private String creationDate;
private String lastUpdate;
private String orderNumber;
public String getCreationDate() {
return this.creationDate;
}
public String getLastUpdate() {
return this.lastUpdate;
}
@NotNull @Size(min=10, max=10)
public String getOrderNumber() {
return this.orderNumber;
}
}
当 Order
的对象在Default
Group下校验时,会校验 注解在getCreationDate
,getLastUpdate
,getOrderNumber
的 @NotNull
约束,getOrderNumber
上的@Size
约束。
当 Order
的对象在Auditable
Group下校验时,会校验 注解在getCreationDate
,getLastUpdate
的 @NotNull
约束。或者是直接表现在Auditable
(以及super接口) 并且属于 Default
Group的约束会在 Auditable
请求下被校验。
当进行一个级联校验时,可以通过Group 转换特性请求一个与原始Group不同的Group。Group转换通过 @ConvertGroup
注解定义。
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
public @interface ConvertGroup {
Class<?> from() default Default.class;
Class<?> to();
@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
属性,那么用于有效验证嵌套元素的组就是在to
属性中定义的对应Group。如果from
属性未定义,则默认为Default Group
。
Group 转换规则不是递归执行的。如果一个规则被匹配,则后续规则不再匹配评估。特别是,如果一组@ConvertGroup
声明将组A
链接到组B
,将组B
链接到组C
,则组A
将被转换为组B
而不是组C
。这样既可以使规则更清晰,也可以让你切换两个组。
? 包含相同from
值的多个Group转换规则是不合法的。在这种情况下,会引发ConstraintDeclarationException
异常。
? 与常规约束声明一样,from
属性不能引用组序列。在这种情况下会引发ConstraintDeclarationException
异常。to
属性可以。然后在验证关联对象之前展开组序列。
验证完成后,在用预期的组验证对象及其级联对象之前,将展开组序列。
关联对象上的组转换发生在已经展开的组上。
@ConvertGroup.from
中引用的组作用于展开的组(即,在组序列被展开之后),而不一定是传递给各种验证方法的组。
@ConvertGroup
中引用的组。将在验证级联对象之前展开,就像调用各种验证方法所做的那样。
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() { [...] }
}
当使用Default
组验证User
实例时,关联的Address
将使用BasicPostal
组验证。当使用Complete
组验证User
实例时,将使用FullPostal
组验证关联的Address
。
Group
转换也可以在容器元素验证期间应用:
public class User {
//注意:注解放置的位置。
Set<
@Valid
@ConvertGroup(from=Default.class, to=BasicPostal.class)
@ConvertGroup(from=Complete.class, to=FullPostal.class)
Address
> getAddresses() { [...] }
}
对于每个类X
。
1、X
的每个超类 Y
,Group Y
包含Group Y
的所有约束。(该规则为递归发现准备了正式的概念)
2、Group X
包含了以下约束:
? a、通过类X
定义的每个约束(未定义一个Group或者没有显示的定义Default Group)。(所有放置在X
上的约束)
? b、由X
实现的任何接口(未注解 @GroupSequence
)上声明的约束(未显示定义一个Group或者没有显示的定义Default Group)。
? c、如果X
有直接超类 Y
,Group Y
中的所有约束。
3、如果 X
没有应用 @GroupSequence
,则**Default
Group** 包含以下约束:
? a、Group X
的所有约束。
? b、如果X
有直接超类 Y
,Group Y
中的所有属于Default
组的约束。
4、如果 X
应用 了@GroupSequence
,则**Default
Group** 包含 属于@GroupSequence
中定义的所有Group的约束。(@GroupSequence
中必须声明 Group X
。)
5、对于每个接口 Z
,Group Z
包含以下约束:(此规则定义了怎么定义非Default
Group)
? a、接口Z
定义的所有约束(未显示定义一个Group或者没有显示的定义Default Group)。所有放置在Z 上的属于Default
的约束。
? b、接口Z
的所有未注解@GroupSequence
的超类接口上定义的约束(未显示定义一个Group)。
? c、类 X
定义的所有约束(显示定义了Group Z
)
? d、由X
实现的任何接口(未注解 @GroupSequence
)上声明的约束(显示定义了Group Z
)。
? e、如果X
有直接超类 Y
, Y
中的所有属于Z
组的约束。
6、对于每个应用了@GroupSequence
的接口Z
,则**Z
Group** 包含 属于@GroupSequence
中定义的所有Group的约束。
?
当校验X时,一个给定组 G
(以接口G
的形式表现)被请求时:
G
的所有约束被校验。@GroupSequence
,接口G
的超类接口表示的Group 会被请求校验。@GroupSequence
,@GroupSequence
中定义的所有组 会被请求校验。
@GroupSequence
中定义的所有组的校验,必须以定义的顺序进行。Group G
是 X
的 Default
Group 的表示(通过@GroupSequence
),则操作是等价的。当 Group G
是 X
的 Default
Group 的表示(通过@GroupSequence
) ,校验规则如下:
@GroupSequence
中定义的所有组(通过接口表现)都会被请求校验。约束能够应用到通用容器(例如Map,List,Optional)中的元素。通过把约束放置在容器的类型参数上。
容器元素约束可以声明在:字段、属性、方法或构造函数的参数、方法返回值。
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) {
}
当一个容器类型被校验时,容器内的所有元素会被校验。该元素的任何容器元素约束都将与该元素承载的任何其他约束一起进行验证。对于容器元素约束,验证组和组序列的规则与其他约束同样适用元素。
当一个容器元素被约束,则校验引擎会调用value extractor
从容器中获取元素。可能是一个值(例如非空Optional),也可能是多个值(例如collection)。
容器元素约束可以应用于内部集合类型。
private Map<String, @NotEmpty List<@ValidAddress Address>> addressesByType;
这种情况,多个 Value Extractor 会被调用。
List<Address>
)的Exctractor 被调用。List<Address>
会校验 @NotEmpty
约束。获取List的Extractor会被调用,Address
对的@ValidAddress
约束 会被校验。泛型类型或泛型方法上的参数类型不支持定义约束。extends
,implements
语句上的类型参数也不支持定义约束。
// 泛型类型的类型参数 不支持
public class NonNullList<@NotNull T> {
}
public class ContainerFactory {
// 泛型方法的类型参数 不支持
<@NotNull T> Container<T> instantiateContainer(T wrapped) { }
}
// extends的类型参数 不支持
public class NonNullSet<T> extends Set<@NotNull T> {
}
除了在类型参数上指定容器元素约束外,还可以声明非泛型容器类型上的容器元素约束。这是通过隐式unwrap
完成的。也就是说,约束并不应用于带注解的容器本身,而是应用于它的元素。隐式unwrap类型有:java.util.OptionalInt
,OptionalLong
和OptionalDouble
以及JavaFX的非泛型属性类型,例如StringProperty
或IntegerProperty
。
为了实现此情况,一个非歧义的Extractor
需要被定义,可以通过@UnwrapByDefault
指定。
如有必要,一个定义在容器上的约束的目标可以通过Unwrap
和Skip
payload 定义
public interface Unwrapping {
public interface Unwrap extends Payload {
}
public interface Skip extends Payload {
}
}
Unwrapping
当应用一个约束(定义在非泛型容器)到以下情况时很有用:
@UnwrapByDefault
(使用Unwrap
)@UnwrapByDefault
(使用Skip
)//NotNull 是作用在容器本身,通过Skip。
@NotNull(payload = Unwrapping.Skip.class)
private StringProperty name;
建议把约束放置在类型参数上 替换 放在容器上。
//建议
List<@Email String> emails;
//不建议
@Email(payload = Unwrapping.Unwrap.class)
List<String> emails;
默认getter 不被考虑方法约束。
静态方法会忽略校验。实例方法要求必须是public ,不能是final。
当关注的方法被调用时,声明的参数约束不会自动引起校验。触发约束校验是集成层的责任,可以通过方法拦截,动态代理等触发校验。
主要约束注解的Target 元注解的正确使用。
有时,验证不仅依赖于单个参数,还依赖于方法或构造函数的几个甚至所有参数。在交叉参数约束的帮助下,可以满足这种要求。
交叉参数约束可以被认为是等价于类级约束的方法验证。两者都可以用来实现基于多个元素的验证要求。当类级约束应用于bean的几个属性时,交叉参数约束应用于可执行元素的多个参数。
与单参数约束不同,交叉参数约束在方法或构造函数上声明。声明交叉参数约束在一个无参数方法或者构造上述时,会抛出ConstraintDeclarationException
异常。
返回值约束也在方法级别声明。为了区分交叉参数约束和返回值约束,在ConstraintValidator
实现中使用@SupportedValidationTarget
注解配置约束目标。
在某些情况下,约束可以应用于可执行方法的参数(即,它是交叉参数约束),也可以应用于返回值。在声明约束时必须指定validationAppliesTo
属性避免歧义。
以下情况可以自动判断:
当一个参数约束校验失败时,为了在ConstraintViolation
中标识关注的参数,Bean Validation定义了javax.validation.ParameterNameProvider
API 用于获取参数名称。
public interface ParameterNameProvider {
/** 获取构造函数的参数名称
*/
List<String> getParameterNames(Constructor<?> constructor);
/** 获取方法的参数名称
*/
List<String> getParameterNames(Method method);
}
一个标准的Bean Validation 实现要实现一个默认的ParameterNameProvider
。一个符合规范的实现必须要么使用Java反射API,要么以以下方式确保与使用反射API的行为兼容性。
@Valid注解用于定义方法(或构造函数)的参数或返回值引起级联验证,当验证@Valid注释的参数或返回值时,对参数或返回值对象声明的约束也会进行验证。
@Valid
被标记,则触发对象约束校验。当对象属性标记 @Valid
,则继续触发级联验证。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) {
}
}
当在继承层次结构中声明方法约束时,需要注意以下规则:
这些规则是由里氏替换原则(Liskov Substitution principle)驱动的,该原则要求无论在哪里使用类型T,都可以在不改变程序行为的情况下使用T的子类型S。
因此,以下规则适用于继承层次结构中方法约束的定义:
本节中描述的规则仅适用于方法,而不适用于构造函数。根据定义,构造函数从不重写超类型构造函数。因此,在验证构造函数调用的参数或返回值时,只应用构造函数本身声明的约束,而不应用超类型构造函数上声明的任何约束。
对于给定的组,应用在给定bean实例上的验证流程以不特定的顺序执行以下约束验证:
Validation routine
被其他验证路径处理了或者被前面一个Group 匹配了。Validation routine
被其他验证路径处理了或者被前面一个Group 匹配了。可访问的字段,getter,associations在 Traversable 属性中定义
穿透验证器主要适用于 JPA 规范,JPA 规范提供一种惰性连接属性,同意实体对象的某些字段被延迟载入,这些被延迟载入的字段须要 JPA 从底层数据库中获取。
Bean Validation 规范通过 TraversableResolver 接口来控制这类字段的存取性。在实际使用中须要先调用该接口中的 isReachable() 方法。假设返回 true,则证明该属性是可存取的。方可进行属性的约束验证。
相同,在进行级联验证时,也须要首先确定所引用的字段或者属性的可存取性方可进行约束的级联验证。
以@Email
作为示例说明 约束的实现。
@Documented
@Constraint(validatedBy = { })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
public @interface Email {
String message() default "{javax.validation.constraints.Email.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String regexp() default ".*";
Pattern.Flag[] flags() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
Email[] value();
}
}
注意:约束的validatedBy
没有指定,则是采用框架的默认实现。具体实现在包 org.hibernate.validator.internal.constraintvalidators.bv
下。
// 参数类型为:Email,则表示
public class EmailValidator extends AbstractEmailValidator<Email> {
private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
//注解中设置的 正则表达式
private java.util.regex.Pattern pattern;
//初始化:获取注解 @Email中的属性。 主要是构造 Pattern 。
@Override
public void initialize(Email emailAnnotation) {
super.initialize( emailAnnotation );
Pattern.Flag[] flags = emailAnnotation.flags();
int intFlag = 0;
for ( Pattern.Flag flag : flags ) {
intFlag = intFlag | flag.getValue();
}
// we only apply the regexp if there is one to apply
if ( !".*".equals( emailAnnotation.regexp() ) || emailAnnotation.flags().length > 0 ) {
try {
pattern = java.util.regex.Pattern.compile( emailAnnotation.regexp(), intFlag );
}
catch (PatternSyntaxException e) {
throw LOG.getInvalidRegularExpressionException( e );
}
}
}
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if ( value == null ) {
return true;
}
// 超类的校验
boolean isValid = super.isValid( value, context );
if ( pattern == null || !isValid ) {
return isValid;
}
//自身的校验。
Matcher m = pattern.matcher( value );
return m.matches();
}
}
进行@
字符之前的字符串的校验。
public class AbstractEmailValidator<A extends Annotation> implements ConstraintValidator<A, CharSequence> {
private static final int MAX_LOCAL_PART_LENGTH = 64;
private static final String LOCAL_PART_ATOM = "[a-z0-9!#$%&'*+/=?^_`{|}~\u0080-\uFFFF-]";
private static final String LOCAL_PART_INSIDE_QUOTES_ATOM = "([a-z0-9!#$%&'*.(),<>\\[\\]:; @+/=?^_`{|}~\u0080-\uFFFF-]|\\\\\\\\|\\\\\\\")";
/**
* Regular expression for the local part of an email address (everything before '@')
*/
private static final Pattern LOCAL_PART_PATTERN = Pattern.compile(
"(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" +
"(\\." + "(" + LOCAL_PART_ATOM + "+|\"" + LOCAL_PART_INSIDE_QUOTES_ATOM + "+\")" + ")*", CASE_INSENSITIVE
);
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if ( value == null || value.length() == 0 ) {
return true;
}
// cannot split email string at @ as it can be a part of quoted local part of email.
// so we need to split at a position of last @ present in the string:
String stringValue = value.toString();
int splitPosition = stringValue.lastIndexOf( '@' );
// need to check if
if ( splitPosition < 0 ) {
return false;
}
String localPart = stringValue.substring( 0, splitPosition );
String domainPart = stringValue.substring( splitPosition + 1 );
if ( !isValidEmailLocalPart( localPart ) ) {
return false;
}
return DomainNameUtil.isValidEmailDomainAddress( domainPart );
}
private boolean isValidEmailLocalPart(String localPart) {
if ( localPart.length() > MAX_LOCAL_PART_LENGTH ) {
return false;
}
Matcher matcher = LOCAL_PART_PATTERN.matcher( localPart );
return matcher.matches();
}
}
以手机号验证为例。
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
//指定由哪个 Validator 验证。
@Constraint(validatedBy={MobileValidator.class})
public @interface Mobile {
String message() default"手机号校验错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
MobileValidator
public class MobileValidator implements ConstraintValidator<Mobile,String> {
//手机号正则表达式
private Pattern p = Pattern.compile("^1[0-9][0-9]\\d{8}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if(value == null){
//如果没有的情况下则认为是正确的
return true;
}
Matcher m = p.matcher(value);
boolean b = m.matches();
return b;
}
}
更简单方式:组合已有约束
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Pattern(regexp = "^1[0-9][0-9]\\d{8}$")
public @interface Mobile {
String message() default"手机号校验错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}