摘抄自技术博文:https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/
概述Bean Validation规范
Bean 是 Java Bean 的缩写,在 Java 分层架构的实际应用中,从表示层到持久化层,每一层都需要对 Java Bean 进行业务符合性验证。然而对于同一个 Java Bean 的对象,在每一层都需要实现同样的验证逻辑时,这将是一项耗时且容易诱发错误的做法。Bean Validation 规范的目标就是避免多层验证的重复性。事实上,开发者更倾向于将验证规则直接放到 Java Bean 本身,使用注解的方式进行验证规则的设计。
图1:Java分层验证
JSR303 规范(Bean Validation 规范)提供了对 Java EE 和 Java SE 中的 Java Bean 进行验证的方式。该规范主要使用注解的方式来实现对 Java Bean 的验证功能,并且这种方式会覆盖使用 XML 形式的验证描述符,从而使验证逻辑从业务代码中分离出来。
图2:Java Bean 验证模型示意图
使用的是hibernate-validator-5.2.0
简单实例:
public class Employee {
@NotNull(message = "The id of employee can not be null")
private Integer id;
@NotNull(message = "The name of employee can not be null")
@Size(min = 1,max = 10,message="The size of employee's name must between 1 and 10")
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Employee employee = new Employee();
employee.setName("Zhang Guan Nan");
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Employee>> set = validator.validate(employee);
for (ConstraintViolation<Employee> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
}
}
输出结果:
2017-10-9 12:45:02 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.0.Final
The size of employee's name must between 1 and 10
The id of employee can not be null
当实际的javabean被使用时,相关的验证器就会对实例进行验证。
验证的步骤:
1) 约束注解的定义
2) 约束验证规则(约束验证器)
3) 约束注解的声明
4) 结束验证流程
约束的定义
约束注解
Bean Validation规范对约束的定义包括两个部分:约束注解、约束验证器。
约束注解:比如@NotNull和@Size就是约束注解
约束验证器:每个约束注解都对应一个约束验证器,约束验证器用来具体的验证Java Bean是否满足注解约束条件。
约束注解例子:
@NotNull(message = "The id of employee can not be null")
private Integer id;
含义:对于字段id,在JavaBean的实例中不能为空。
Bean Validation规范内嵌的约束注解定义:
@Null 验证对象是否为空
@NotNull 验证对象是否不为空
@AssertTrue 验证Boolean对象时候为True
@AssertFalse 验证Boolean对象时候为False
@Min 验证Number和String对象是否大于指定值
@Max 验证Number和String对象是否小于指定值
@DecimalMin 验证Number和String对象时候大于指定值,小数存在精度
@DecimalMax 验证Number和String对象是否小于指定值,小数存在精度
@Size 验证对象(Array,Collection,Map,String)长度是否在给定的范围内
@Digits 验证Number和String的构成是否合法
@Past 验证Date和Calendars对象是否在给定时间之前
@Future 验证Date和Calendar对象是否在给定赶时间之后
@Pattern 验证String对象是否符合正则表达式
除了使用给定的内嵌注解,也可以扩展给定的API,定义自己的注解,符合自己的实际业务需求。
约束注解的定义和普通的注解定义一样,典型的注解定义应该包含一下几个部分:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({})// 约束注解应用的目标元素类型
@Retention(RetentionPolicy.RUNTIME) //约束注解应用的时机
@Constraint(validatedBy = {})//与约束注解相关的验证器
public @interface ConstraintName {
String message() default "";//约束注解验证时输出的消息
Class<?>[] groups() default {};//约束注解验证时所属的组别
Class<? extends Payload>[] payload() default {};//约束注解的有效负载
}
其中,约束注解应用的目标元素类型包括:METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER。METHOD约束相关的getter方法,FIELD约束相关的属性,TYPE约束具体的Java Bean,ANNOTATION_TYPE用在组合约束中,PARAMETER参数约束,CONSTRUCTOR构造器约束。有效负载通常用来将一些元数据信息与该约束注解相关联,常用的一种情况是用负载表示验证结果的严重程度。
实例:
验证字符串非空的约束注解的定义
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { NotEmptyValidator.class })
public @interface NotEmpty {
String message() default "this string may be empty";//约束注解验证时输出的消息
Class<?>[] groups() default {};//约束注解验证时所属的组别
Class<? extends Payload>[] payload() default {};//约束注解的有效负载
}
注解定义完成后,要实现与注解相关联的验证器,需要实现接口:javax.validation.ConstraintValidator,改接口的定义如下:
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);
boolean isValid(T value, ConstraintValidatorContext context);
}
initialize:对验证器实例化必须在验证器实例在被使用之前调用,参数是约束注解。
isValid:约束验证的主体,value是需要验证的实例,context代表约束执行的上下文。
对应的验证器实现:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {
@Override
public void initialize(NotEmpty constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
} else if (value.length() < 1) {
return false;
}
return true;
}
}
测试:
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Employee {
@NotNull(message = "The id of employee can not be null")
private Integer id;
@NotNull(message = "The name of employee can not be null")
@Size(min = 1, max = 10, message = "The size of employee's name must between 1 and 10")
private String name;
@NotEmpty(message = "不能为空(自己定义的约束注解)")
private String addr;
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Employee emp = new Employee();
emp.setName("asdd zxcv qwe");
ValidatorFactory validatorFactory = Validation
.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<Employee>> validate = validator.validate(emp);
for (ConstraintViolation<Employee> constraintViolation : validate) {
System.out.println(constraintViolation.getMessage());
}
}
}
结果:
2017-10-9 13:50:05 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.0.Final
不能为空(自己定义的约束注解)
The id of employee can not be null
The size of employee's name must between 1 and 10
多值约束
多值约束是Bean Validation的一个特性:表示,对于同一个目标元素,在进行约束注解声明时可以使用不同的属性达到对该元素进行多值验证的目的。实现多值约束只需在约束注解定义时同时定义一个List。使用该约束注解时,Bean Validation 将 value 数组里面的每一个元素都处理为一个普通的约束注解,并对其进行验证,所有约束条件均符合时才会验证通过。
实例:
验证某一字符串是否包含指定的字符串。
约束注解定义:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { PatternOfStringValidator.class })
public @interface PatternOfString {
String mustContainLetter();
String message() default "the pattern may be not right";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
PatternOfString[] value();
}
}
对应的验证器:
public class PatternOfStringValidator implements ConstraintValidator<PatternOfString, String> {
private String letterIn;
@Override
public void initialize(PatternOfString patternOfString) {
this.letterIn = patternOfString.mustContainLetter();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value.contains(letterIn)) {
return true;
}
return false;
}
}
测试:
public class Employee {
@NotNull(message = "The id of employee can not be null")
private Integer id;
@NotNull(message = "The name of employee can not be null")
@Size(min = 1, max = 10, message = "The size of employee's name must between 1 and 10")
private String name;
@NotEmpty(message = "不能为空(自己定义的约束注解)")
private String addr;
@PatternOfString.List({
@PatternOfString(mustContainLetter = "CH", message = "It does not belong to China"),
@PatternOfString(mustContainLetter = "MainLand", message = "It does not belong to MainLand") })
private String palce;
public String getPalce() {
return palce;
}
public void setPalce(String palce) {
this.palce = palce;
}
public String getAddr() {
return addr;
}
public void setAddr(String addr) {
this.addr = addr;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
Employee emp = new Employee();
emp.setName("asdd zxcv qwe");
emp.setPalce("C");
ValidatorFactory validatorFactory = Validation
.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<Employee>> validate = validator.validate(emp);
for (ConstraintViolation<Employee> constraintViolation : validate) {
System.out.println(constraintViolation.getMessage());
}
}
}
结果:
不能为空(自己定义的约束注解)
The size of employee's name must between 1 and 10
It does not belong to MainLand
It does not belong to China
The id of employee can not be null
组合约束
组合约束是另外一种特性,Bean Validation 规范允许将不同的约束进行组合来创建级别较高且功能较多的约束,从而避免原子级别约束的重复使用。比如:
@NotNull(message = “The name of employee can not be null”)
@Size(min = 1, max = 10, message = “The size of employee’s name must between 1 and 10”)
private String name;
其实际意义等同于,将@NotNull和@Size(min = 1)的组合形式,因此,可以定义组合约束NotEmptyNew:
@NotNull
@Size(min = 1)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { NotEmptyValidator.class })
public @interface NotEmptyNew {
String message() default "this string may be empty";//约束注解验证时输出的消息
Class<?>[] groups() default {};//约束注解验证时所属的组别
Class<? extends Payload>[] payload() default {};//约束注解的有效负载
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@interface List {
NotEmptyNew[] value();
}
}
约束的声明和验证流程
验证流程
Bean Validation 规范对 Java Bean 的验证流程如下:在实际使用中调用 Validator.validate(JavaBeanInstance) 方法后,Bean Validation 会查找在 JavaBeanInstance上所有的约束声明,对每一个约束调用对应的约束验证器进行验证,最后的结果由约束验证器的 isValid 方法产生,如果该方法返回 true,则约束验证成功,否则验证失败。验证失败的约束将产生约束违规对象(ConstraintViolation 的实例)并放到约束违规列表中。验证完成后所有的验证失败信息均能在该列表中查找并输出。
前提条件
1) 如果验证的是属性(getter 方法),那么必须遵从 Java Bean 的命名习惯(JavaBeans 规范);
2) 静态的字段和方法不能进行约束验证;
3) 约束适用于接口和基类;
4) 约束注解定义的目标元素可以是字段、属性或者类型等;
5) 可以在类或者接口上使用约束验证,它将对该类或实现该接口的实例进行状态验证;
6) 字段和属性均可以使用约束验证,但是不能将相同的约束重复声明在字段和相关属性(字段的 getter 方法)上。
Object Graph验证
Object Graph为对象的拓扑结构,比如对象的引用关系,如果类A引用的类B,则在对类A进行实例约束验证时,也需要对类B进行实例约束验证,叫做验证的级联性,使用@Valid注解。
public class Person {
@NotEmpty
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Order {
@Valid
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
在对 Order 的实例进行验证时,只有当在 Order 引用的对象 Person 前面声明了注解 @Valid,才对 Person 中 name 字段的 @NotEmpty 注解进行验证,否则将不予验证。
组验证
组定义了约束的子集。对于一个给定的 Object Graph 结构,有了组的概念,则无需对该 Object Graph 中所有的约束进行验证,只需要对该组定义的一个子集进行验证即可。完成组别验证需要在约束声明时进行组别的声明,否则使用默认的组 Default.class。组使用接口方式定义。
interface GroupA {}
public class User {
@NotEmpty(message = "firstname may be empty")
private String firstName;
@NotEmpty(message = "middlename may be empty", groups = Default.class)
private String midleName;
@NotEmpty(message = "lastname may be empty", groups = GroupA.class)
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getMidleName() {
return midleName;
}
public void setMidleName(String midleName) {
this.midleName = midleName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public static void main(String[] args) {
User user = new User();
ValidatorFactory validatorFactory = Validation
.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<User>> validate = validator.validate(user,
GroupA.class);
for (ConstraintViolation<User> constraintViolation : validate) {
System.out.println(constraintViolation.getMessage());
}
}
}
结果:
2017-10-9 14:53:05 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.0.Final
lastname may be empty
在类 User 中需要验证的字段上声明验证时所属的组别属性,如(groups=GroupA.class), 然后在 main 函数中调用 validator.validate(user,GroupA.class)) 方法,在此必须指定需要验证的组别。如果不显示指明,则是默认的组别。验证器只会验证类 User 的 lastname 字段,如果使用 validator.validate(user)),则会使用 Default.class 组别,从而验证 firstname 和 middlename 字段。需要注意的是:组也有继承的属性。对某一组别进行约束验证的时候,也会对其所继承的基类进行验证。
组验证,可以进行隐式定义,实例:
interface Animal {
@NotEmpty(message = "name may be empty")
String getName();
@NotEmpty(message = "ownername may be empty")
String getOwnerName();
}
public class Dog implements Animal {
private String name;
private String ownername;
private String type;
@NotEmpty(message = "type of the dog may be empty")
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getName() {
return null;
}
public String getOwnerName() {
return null;
}
public static void main(String[] args) {
Dog dog = new Dog();
ValidatorFactory vf = Validation.buildDefaultValidatorFactory();
Validator validator = vf.getValidator();
Set<ConstraintViolation<Dog>> set = validator.validate(dog,
Animal.class);
for (ConstraintViolation<Dog> constraintViolation : set) {
System.out.println(constraintViolation.getMessage());
}
}
}
这样在对类 Dog 的实例进行验证的时候,如果使用默认的组别(Default.class),则 name,ownername 和 type 都将进行验证;如果使用 Animal 的组别,如清单 18 所示,则只会对 name 和 ownername 属性进行验证。
组序列
默认情况下,不同的组别的验证顺序是无序的,但是某些情况下,验证的顺序比较重要,比如说:第二个组中的约束验证要依赖于一个稳定环境,但是这个稳定的环境是由第一个验证提供的,另外一种情况是,某个组别的验证比较耗时,所以打算放在最后验证。一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。
interface GroupA {}
interface GroupB {}
@GroupSequence({ Default.class, GroupA.class, GroupB.class })
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 midleName;
@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 validatorFactory = Validation
.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<User>> validate = validator.validate(user,
Group.class);
for (ConstraintViolation<User> constraintViolation : validate) {
System.out.println(constraintViolation.getMessage());
}
}
}
结果:
2017-10-9 15:41:26 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 5.2.0.Final
middlename may be empty
firstname may be empty
从输出结果可以看出,该验证将不再为属于 GroupA 和 GroupB 的约束进行验证,因为属于组序列(Group.class)中前面位置的 Default 组验证失败。只有当在 main 函数加入如下代码片段使属于 Default 组别的验证通过后,方可进行后续组别(GroupA,GroupB)的验证。