以下内容转载自:https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/
Bean Validation规范介绍
JSR303 规范(Bean Validation 规范)提供了对 Java EE 和 Java SE 中的 Java Bean 进行验证的方式。该规范主要使用注解的方式来实现对 Java Bean 的验证功能,并且这种方式会覆盖使用 XML 形式的验证描述符,从而使验证逻辑从业务代码中分离出来。javax.validation是JSR303规范,而hibernate-validator是则是规范的具体实现。
POM文件中引入hibernate-validator即可使用JSR303规范:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>4.3.0.Final</version> </dependency>
或者普通工程添加相关jar包:hibernate-validator 、jboss-logging 、 validation-api这些包吧.
入门案例:
先将最基本的Bean Validation封装成一个简单的工具类,方便使用,案例学习。
Validation API简单封装的ValidationUtils.java
public class ValidationUtils { public static Validator getValidator(){ return validator; } static Validator validator; static{ ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); validator=validatorFactory.getValidator(); } }
定义我们要校验的Java Bean:
Employee.java类
@Setter @Getter @NoArgsConstructor //lombok注解 节约篇幅 代码整洁 public class Employee { @NotNull(message = "员工姓名不能为空") @Size(min = 1,max = 10,message = "员工名字长度必须在10个字母以内") private String name; @NotNull(message = "员工ID不能为空") private Integer id; }
使用方式:
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin6666"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
查看结果,可以看到这就是Bean Validation的简单使用,基于注解方式还是很方便的。
验证流程
完成Java Bean的验证流程通常分为四个步骤:
1.约束注解定义
2.约束验证器验证规则定义
3.约束注解声明
4.约束验证流程
自定义实现JSR303规范----@NotEmpty
Bean Vadalition中并没有字符串为空的注解验证,Hibernate-validator扩展了@NotEmpty来校验 字符串不为空 ,自己实现一个字符串不为空的校验规则
按照上面的流程,首先需要约束注解的定义,仿照@NotNull复制一个NotEmpty;
import static java.lang.annotation.ElementType.*; //静态导包 @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //注解应用的目标类型 @Retention(RetentionPolicy.RUNTIME) //注解应用时期 @Documented @Constraint(validatedBy = {NotEmptyValidator.class}) //注解关联的验证器,JSR303的注解 public @interface NotEmpty { String message() default ""; //验证时输出信息 Class<?>[] groups() default { }; //验证时所属的组别 Class<? extends Payload>[] payload() default {}; }
其中ValidatedBy就是自定义的验证器的指向。第二步编写自定义验证器NotEmptyValidator
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.trim().length()==0) return false; else return true; } }
第三步声明约束注解(代码为增量形式,重复的就忽略了)
@NotEmpty(message = "员工职位不能为空") private String job;
第四步验证约束流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName(""); employee.setId(18); employee.setJob(""); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
验证结果就不贴了,在自定义验证器中输出信息,发现确实调用了自定义的验证规则;我们并没有对@NotEmpty添加到Validator中,只是在实体类上声明了,就会找到这个注解,进而根据Constraint注解上的validatedBy,找到自定义的验证器,从而完成验证流程!
Bean Validation内嵌的约束注解
非空性验证两个: @NotNull 不为空 ; @Null 为空
布尔型验证两个:@AssertTrue 布尔值为true ; @AssertFalse 布尔值不为空
日期类型验证两个:@Past 日期必须为过去的日期 ; @Future 日期必须为未来某个日期
正则表达式验证一个:@Pattern
数值类型验证若干:@Min String或Number类型大于等于该值 ; @Max String或Number类型小于等于该值 这两种类型的值不支持小数校验
@DecimalMin String或Number类型大于等于该值,支持小数 ; @DecimalMax String或Number类型小于等于该值,支持小数
集合验证类型一个:@Size String、Array、Collection、Map长度类型在设定范围内
多值约束问题
Bean Validation的一个特性:多值约束。
@NotNull等内嵌注解最下面都会有@interface List这样一个注解,就是用来实现多值约束。 补充说明:一个注解无法再同一个位置标注两次,编译报错Duplicate Annotaion,所以就有了内部注解,此外还可以通过@Repeatable容器的概念来实现多个注解标注。
举个栗子,我们需要判断一个字符串包含多个子串,类似地一个数组、集合、map包含这种属性也是一样的;
按照上面流程,定义约束注解 @Dictionary
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {DictionaryValidator.class}) public @interface Dictionary { String skillValue(); String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default {}; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @interface List { Dictionary[] value(); } }
自定义约束验证器规则编写:
public class DictionaryValidator implements ConstraintValidator<Dictionary,String> { String skillValue=null; @Override public void initialize(Dictionary constraintAnnotation) { skillValue=constraintAnnotation.skillValue(); } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if(value==null){ return false; } else if(value.trim().length()==0){ return false; }else if(value.contains(skillValue)){ return true; } return false; } }
第三步声明注解,和@Repeatable有异曲同工之妙
@Dictionary.List(value = { @Dictionary(message = "该员工不会java",skillValue = "java"), @Dictionary(message = "该员工不会vue",skillValue = "vue") }) private String skill;
第四步 验证约束
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
得出结论:达到了我们的目的,校验skill属性包含 java vue子字符串,其实@Pattern编写正则表达式就可以达到这样的目的,但是集合 数组等类型多值判断时就需要多值校验了,还是有用武之处的!
组合约束
假设现在校验员工身高,正常身高假设在1米以上3米以下,@Min(value=100) @Max(value=300)可能就完成这样一个简单的校验规则。Bean Validation新特性,组合校验。
import static java.lang.annotation.ElementType.*; @NotNull(message = "如实禀报身高") @Min(message = "正常人身高在1米以上",value = 100) @Max(message = "正常人身高在3米以下",value = 300) @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) //注解应用的目标类型 @Retention(RetentionPolicy.RUNTIME) //注解应用时期 @Documented @Constraint(validatedBy = {}) public @interface Height { String message() default ""; //验证时输出信息 Class<?>[] groups() default { }; //验证时所属的组别 Class<? extends Payload>[] payload() default {}; }
声明注解@Height
@Height private Integer height;
校验流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(183); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
补充记录:测试时候发现,@Height注解报错提示信息没有用,因为组合的注解没有通过,不是@Height注解校验不通过,可以再自定义约束验证器,这样校验不通过就能抛出@height中的message;
Bean Validation验证规则流程
调用 Validator.validate(beanInstance) 方法后,Bean Validation 会查找在 beanInstance上所有的约束声明(注解式),对每一个约束调用对应的约束验证器进行验证,由约束验证器的 isValid 方法产生,如果该方法返回 true,则约束验证成功,否则验证失败。验证失败生成约束违规对象(ConstraintViolation 的实例)并放到约束违规列表中。验证完成后所有的验证失败信息均能在该列表中查找并输出。
条件:静态方法、字段无法约束验证;可以在类或者接口上使用约束验证,它将对该类或实现该接口的实例进行状态验证;
级联验证方式
对象之间存在级联关系,A类存在属性B,对A校验的同时,如果需要对B完成校验,在B上添加@Valid即可;
举个栗子,假设Address存在属性Employee,在对Address实例校验时,在Employee上标注@Valid;
@Setter @Getter public class Address { @Valid Employee employee; @NotNull String location; }
校验流程
public static void main(String[] args) { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(95); Address address = new Address(); address.setEmployee(employee); Set<ConstraintViolation<Address>> violations = ValidationUtils.getValidator().validate(address); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
输出结果: 注释掉@Valid,employee属性就不会校验
Bean Validation注解生效
Bean Validation的注解可以在属性上 、在getter方法上生效,至于为什么在setter方法上没法生效 这个可能要成为谜题了;
测试getter上生效
private Date birth; public void setBirth(String date) throws ParseException { this.birth=new SimpleDateFormat("yyyy/MM/dd").parse(date); } @Past public Date getBirth(){ return birth; }
验证流程:
public static void main(String[] args) throws ParseException { Employee employee = new Employee(); employee.setName("lvbinbin"); employee.setId(18); employee.setJob(""); employee.setSkill("java vue"); employee.setHeight(183); employee.setBirth("9102/12/31"); Set<ConstraintViolation<Employee>> violations = ValidationUtils.getValidator().validate(employee); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } }
验证结果:
组
Bean Validation中提出了组的概念,这点和Jackson中的@JsonView很类似,我只展示我需要的属性;而Bean Validation中组,给每个校验属性分组,我只校验我指定的组中包含的属性,我不显示的指定组名,那就校验默认的组,Default组;
使用说明一:默认不指定组的话,下面Validator.validate(beanInstance)和Validator.validate(beanInstance,Default.class)这两个输出是一样的,证明了默认组别叫Default; 这里的Default是javax.validation包中的,其他很多地方有这个类,不要到错了!
@Setter @Getter public class User { @NotNull private String groupfieldA1; @NotNull private String groupfieldA2; @NotNull private String groupfieldB1; @NotNull private String groupfieldB2; public static void main(String[] args) { User user = new User(); //Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user); Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, Default.class); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } } }
使用说明二:
组别一定要采用接口定义,组别不是接口会抛出异常:javax.validation.ValidationException: HV000045: A group has to be an interface
组的接口不一定要重新再定义,哪怕使用已有的接口也没有问题,我觉得只是作为标识来使用;
public static interface groupA{} @NotNull(groups = {groupA.class}) private String groupfieldA1;
这样通过Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupA.class)校验,就只会针对groupA组的属性校验,即属性groupfieldA1
使用说明三:
组接口继承,属性也会继承被校验; 这点类比JsonView
public static interface groupA{} public static interface groupB extends groupA{} @NotNull(groups = {groupA.class}) private String groupfieldA1; @NotNull(groups = {groupB.class}) private String groupfieldA2;
这样通过Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupB.class)校验,组别B的属性校验,组别A的属性也会被校验;
组序列 @GroupSequence
Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, groupA.class,groupB.class)支持多个组进行校验,而且组与组之间互不影响,校验组别顺序只与输入的组别顺序级groupA>groupB有关,组序列为了满足组与组之间互相存在影响而出现,保证了顺序以及避免不必要的校验(加入校验B依赖于校验A,校验A都没通过,校验B就没必要校验了)
@Setter @Getter public class User { public static interface groupA{} public static interface groupB{} @GroupSequence(value = {groupA.class,groupB.class}) public static interface group {} @NotNull(groups = {groupA.class}) private String groupfieldA1; @NotNull(groups = {groupB.class}) private String groupfieldA2; @NotNull private String groupfieldB1; @NotNull private String groupfieldB2; public static void main(String[] args) { User user = new User(); //Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user); Set<ConstraintViolation<User>> violations = ValidationUtils.getValidator().validate(user, group.class); for (ConstraintViolation constraintViolation:violations) { System.out.println("当前有问题属性为:"+constraintViolation.getPropertyPath().toString()); System.out.println("报错原因为:"+constraintViolation.getMessage()); } } }
输出截图: @GroupSequence保证了校验的顺序,以及存在依赖关系时的校验,第一个都没有通过,后续不再校验
Bean Validation接口规范
MessageInterpolator接口:消息解析器,用来将验证过程中失败的消息以可读的方式传递给调用者; Bean Validation规范提供一个默认实现,configuration.getDefaultMessageInterpolator();
用户自定义消息解析器只需要实现MessageInterpolator接口;
Bean Validation 规范的输出消息默认从类路径下的 ValidationMessage.properties 文件中读取,用户也可以在约束注解声明的时候使用 message 属性指定消息内容。
Configuration接口:收集上下文环境中的配置信息,主要用来计算如何给定正确的 ValidationProvider,并将其委派给 ValidatorFactory 对象。
参考文档
BeanValidation : https://www.ibm.com/developerworks/cn/java/j-lo-beanvalid/