Java Bean Validation规范

发布时间:2024年01月02日

Java中的数据校验

? 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/

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,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(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、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

约束(Constraint)定义

约束是一系列约束注解与约束校验实现的组合。约束注解可以应用在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 约束注解可以应用于:

  • FIELD for constrained attributes
  • METHOD for constrained getters and constrained method return values
  • CONSTRUCTOR for constrained constructor return values
  • PARAMETER for constrained method and constructor parameters
  • TYPE for constrained beans
  • ANNOTATION_TYPE for constraints composing other constraints
  • TYPE_USE for container element constraints

Cross-parameter约束注解可以应用于:

  • METHOD
  • CONSTRUCTOR
  • ANNOTATION_TYPE for cross-parameter constraints composing other cross-parameter constraints

一个Constraint注解可以同时应用于Generic 约束注解 和 Cross-parameter约束注解

Constraint 定义属性

在指定一个注解应用于Java Bean的同时,需要指定属性,这些属性映射为注解元素。注解元素包括:message, groups, validationAppliesTopayload ,为保留名称,注解元素命名不能以valid 开头,constraint 可以使用其他元素名称作为其属性。

message

每个约束注解都必须定义一个String类型的message 元素。

String message() default "{com.acme.constraint.MyConstraint.message}";

message元素的值用于描述错误信息。

groups

groups用于定义约束定义关联到哪个处理Group,类型为Class<?>[]

Class<?>[] groups() default {};

默认值为空数组。如果未定义,则表示使用Default 组。

payload

类型为 Payload[],默认值为空数组。

Class<? extends Payload>[] payload() default {};

public interface Payload {
}

payload通常用于定义关联到约束注解的元数据,在client进行校验时使用。

validationAppliesTo

ConstraintTarget 枚举类型,用于指定注解应用于什么类型。

ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
public enum ConstraintTarget {
  /**
    当没有方法和构造函数时,表示type,field。当方法和构造函数没有参数时,表示返回值,当没有返回值时,表示参数。
  */
  IMPLICIT,

  RETURN_VALUE,
  PARAMETERS
}

示例:@Min

@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约束的groupspayloadvalidationAppliesTo属性 会继承主约束(上例: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 要求:

  • 非泛型类型。
  • 或者T是泛型类型,但是必须是未界限的通配符<?>

@SupportedValidationTarget

ConstraintValidatorFactory

约束校验实现的实例是由ConstraintValidatorFactory创建的。由谁创建就由谁释放,不由创建者释放则会出错误。

public interface ConstraintValidatorFactory {
 
  <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key);
 
  void releaseInstance(ConstraintValidator<?, ?> instance);
}

Value extractor定义

容器元素约束的校验需要从容器获取元素,容器元素的获取通过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);
	}
}

@ExtractedValue

用于指示一个 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;
}

@UnwrapByDefault

内置 value extractors

示例

// 注意:@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名称转成字段名称。

Graph校验

除了支持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的校验 会触发 组 DefaultBillable 的校验。则 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 组

默认情况下,校验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下校验时,会校验 注解在getCreationDategetLastUpdategetOrderNumber@NotNull约束,getOrderNumber上的@Size 约束。

Order的对象在Auditable Group下校验时,会校验 注解在getCreationDategetLastUpdate@NotNull约束。或者是直接表现在Auditable (以及super接口) 并且属于 Default Group的约束会在 Auditable 请求下被校验。

Group 转换

当进行一个级联校验时,可以通过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 的每个超类 YGroup 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、对于每个接口 ZGroup 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直接超类 YY 中的所有属于Z的约束。

6、对于每个应用了@GroupSequence 的接口Z,则**Z Group** 包含 属于@GroupSequence 中定义的所有Group的约束。

?

当校验X时,一个给定组 G (以接口G 的形式表现)被请求时:

  • 属于组 G 的所有约束被校验。
  • 如果 接口 G 未应用@GroupSequence ,接口G的超类接口表示的Group 会被请求校验。
  • 如果 接口 G 应用了@GroupSequence@GroupSequence中定义的所有组 会被请求校验。
    • @GroupSequence中定义的所有组的校验,必须以定义的顺序进行。
    • 如果某个组校验失败,则后续组的校验被终止。
  • 如果 Group GXDefault Group 的表示(通过@GroupSequence),则操作是等价的。

Group GXDefault 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 会被调用。

  • 获取Map值(List<Address>)的Exctractor 被调用。
  • 对于每个List<Address> 会校验 @NotEmpty约束。获取List的Extractor会被调用,
  • 每个 Address 对的@ValidAddress 约束 会被校验。

泛型类型或泛型方法上的参数类型不支持定义约束。extendsimplements 语句上的类型参数也不支持定义约束。

// 泛型类型的类型参数 不支持
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完成的。也就是说,约束并不应用于带注解的容器本身,而是应用于它的元素。隐式unwrap类型有:java.util.OptionalIntOptionalLongOptionalDouble以及JavaFX的非泛型属性类型,例如StringPropertyIntegerProperty

为了实现此情况,一个非歧义的Extractor需要被定义,可以通过@UnwrapByDefault 指定。

如有必要,一个定义在容器上的约束的目标可以通过UnwrapSkip payload 定义

public interface Unwrapping {
    public interface Unwrap extends Payload {
    }

    public interface Skip extends Payload {
    }
}

Unwrapping当应用一个约束(定义在非泛型容器)到以下情况时很有用:

  • 容器元素。没有Value Extractor 标记了@UnwrapByDefault (使用Unwrap)
  • 容器本身。有一个Value Extractor 标记了@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 元注解的正确使用。

交叉参数(Cross-Parameter)约束

有时,验证不仅依赖于单个参数,还依赖于方法或构造函数的几个甚至所有参数。在交叉参数约束的帮助下,可以满足这种要求。

交叉参数约束可以被认为是等价于类级约束的方法验证。两者都可以用来实现基于多个元素的验证要求。当类级约束应用于bean的几个属性时,交叉参数约束应用于可执行元素的多个参数。

与单参数约束不同,交叉参数约束在方法或构造函数上声明。声明交叉参数约束在一个无参数方法或者构造上述时,会抛出ConstraintDeclarationException异常。

返回值约束也在方法级别声明。为了区分交叉参数约束和返回值约束,在ConstraintValidator实现中使用@SupportedValidationTarget注解配置约束目标。

在某些情况下,约束可以应用于可执行方法的参数(即,它是交叉参数约束),也可以应用于返回值。在声明约束时必须指定validationAppliesTo属性避免歧义。

以下情况可以自动判断:

  • 带参数,返回值为void 类型。
  • 无参数,返回值不为void 类型。
  • 不是方法、构造函数,是字段或者属性。
命名参数

当一个参数约束校验失败时,为了在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注释的参数或返回值时,对参数或返回值对象声明的约束也会进行验证。

  • null 参数或 null 返回值忽略。
  • 校验是递归的,当@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。

因此,以下规则适用于继承层次结构中方法约束的定义:

  • 在子类型中,不能在覆盖或实现的方法上声明参数约束,也不能为参数标记级联验证。这将加强调用方需要满足的先决条件。
  • 在子类型中,覆盖或实现一个定义在多个并行类型(例如不互相继承的2个接口,继承的类未实现另一个接口)中的方法,不能为该方法声明参数约束,也不能为参数标记级联验证。这同样是为了避免调用方要满足的前提条件被意外加强。
  • 在子类型中,可以在覆盖或实现的方法上声明返回值约束,也可以标记级联验证。验证后,将验证所请求方法的所有返回值约束,无论它们在层次结构中的何处声明。这可能只是对调用方保证的方法后置条件进行了加强,而不是削弱。
  • 不要在一条继承线上声明多于一次的级联验证。如果超类已声明级联验证,则覆盖(或实现)方法不需要再标记。

本节中描述的规则仅适用于方法,而不适用于构造函数。根据定义,构造函数从不重写超类型构造函数。因此,在验证构造函数调用的参数或返回值时,只应用构造函数本身声明的约束,而不应用超类型构造函数上声明的任何约束。

Validation 流程( routine)

对于给定的组,应用在给定bean实例上的验证流程以不特定的顺序执行以下约束验证:

  • 对于所有可访问的字段(包括超类中的字段),执行匹配指定Group的字段级别的验证,除非指定的验证约束已经在Validation routine被其他验证路径处理了或者被前面一个Group 匹配了。
  • 对于所有可访问的getter(包括超类中的),执行匹配指定Group的getter级别的验证,除非指定的验证约束已经在Validation routine被其他验证路径处理了或者被前面一个Group 匹配了。
  • 执行类级别的校验
  • 可访问的级联关联验证。

可访问的字段,getter,associations在 Traversable 属性中定义

对象Graph校验

穿透验证器(TrversableProperty)

穿透验证器主要适用于 JPA 规范,JPA 规范提供一种惰性连接属性,同意实体对象的某些字段被延迟载入,这些被延迟载入的字段须要 JPA 从底层数据库中获取。

Bean Validation 规范通过 TraversableResolver 接口来控制这类字段的存取性。在实际使用中须要先调用该接口中的 isReachable() 方法。假设返回 true,则证明该属性是可存取的。方可进行属性的约束验证。

相同,在进行级联验证时,也须要首先确定所引用的字段或者属性的可存取性方可进行约束的级联验证。

ConstraintValidator算法

ValueExtractor原理

附录

自定义约束的实现示例

@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 下。

EmailValidator

// 参数类型为: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();
	}
}
父类AbstractEmailValidator

进行@字符之前的字符串的校验。

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 {};
}
文章来源:https://blog.csdn.net/demon7552003/article/details/135322695
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。