天天看点

Hibernate Validator实践之一

在后台的业务逻辑中,对数据值的校验在各层都存在(展示层,业务层,数据访问层等),并且各层校验的规则又不尽相同,如下图所示

Hibernate Validator实践之一

注:该图片来自于Hibernate Validator官网

在各层中重复的校验逻辑既导致了不必要的资源消耗,还使得逻辑不够单一(每层都夹杂着校验的逻辑),JSR 303 Bean Validation就是在这种背景下产生的一个数据验证的J2EE规范。而我们这篇文中将要介绍的Hibernate Validator则是JBoss社区开源的一个JSR 303  Bean Validation规范的优秀实践。

Hibernate Validator实践之一

注:该图片来自于Hibernate Validator官网

下面我们以一个具体的列子讲述下如何在我们的工程中使用Hibernate Validator

首先我们定义了一个结构体Person,具体的定义如下

public class Person { @NotNull private String name; @Min(value = 1) private int age; @NotNull(groups = Intf1.class) @Size(min = 1, max = 3, groups = Intf2.class) private String group; @GenderCase(value = GenderType.FEMALE) private GenderType gender; @Max(100) public int getAge() { return age; } @Max(50) public int getAgeOther() { return age + 2; } @Max(50) public int getAgeOther(int num) { return 51; } public Person(String name, int age) { this.name = name; this.age = age; } }

其中该类中用到Max,Min,NotNull,Size等都是JSR 303中内置的约束条件(constraint),GenderCase是自定义的约束条件,这个在后面会介绍。

对Bean进行约束校验,首先需要先获得一个校验器实例

ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator();

(一)如何对JSR 303 内置的约束条件进行校验

根据上面Person结构体的定义,我们看个简单的例子

Person person = new Person(null, 20); Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person); assertEquals(1, constraintViolations.size()); System.out.println(constraintViolations);

在Person结构体定义中,name不可以为null,这里我们故意构造了一个name的null的Person实例,结果在控制台输入的结果如下:

[ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=name, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

从上面的例子可以看出,只需要在定义结构体中将JSR 303 内置的约束注解添加到对应的属性上,通过Validator实例的validate方法,如果返回的Set集合不为空,通过遍历集合便可知哪些属性的值非法。

Bean Validation 中的 constraint

表 1. Bean Validation 中内置的 constraint

@Null 被注释的元素必须为 null

@NotNull 被注释的元素必须不为 null

@AssertTrue 被注释的元素必须为 true

@AssertFalse 被注释的元素必须为 false

@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max, min) 被注释的元素的大小必须在指定的范围内

@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内

@Past 被注释的元素必须是一个过去的日期

@Future 被注释的元素必须是一个将来的日期

@Pattern(value) 被注释的元素必须符合指定的正则表达式

表 2. Hibernate Validator 附加的 constraint

@Email 被注释的元素必须是电子邮箱地址

@Length 被注释的字符串的大小必须在指定的范围内

@NotEmpty 被注释的字符串的必须非空

@Range 被注释的元素必须在合适的范围内

注:上面两个表格中的内容来自于这里,

如果有结构体嵌套,只需要在复合属性上通过Valid注解,则可以递归的进行校验。

(二)validateValue与validateProperty

通过javax.validation.Validator接口类的定义可知,校验的方法有三个,分别是validate,validateProperty,validateValue。其中validate会将所有的属性进行约束校验,而validateProperty是针对某一个具体的属性进行校验,validateValue是对具体的某一个属性和特定的值进行校验。具体看下面的两个例子

第一个例子:

Person person = new Person(null, 101); Set<ConstraintViolation<Person>> constraintViolations = validator.validateProperty(person, "age"); assertEquals(1, constraintViolations.size()); System.out.println(constraintViolations);

根据上面的结构定义可以看出来在Person结构体中对age的约束有两个,一个是最小值为1,另一个是个getter方法上的约束最大不能超过100,执行上面的逻辑输出的结果为:

[ConstraintViolationImpl{interpolatedMessage='最大不能超过100', propertyPath=age, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.Max.message}'}]

可见validateProperty不光是对field的值进行校验,还会对getter方法也进行校验。

第二个例子:

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "name", null); assertEquals(1, constraintViolations.size()); System.out.println(constraintViolations);

第二个例子表示在执行validateValue时,给定一个结构体定义,field的名称,看该特定的值是否符合约束,执行的结果如下:

[ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=name, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

(三)约束条件的分组

在JSR 303 中定义了group的概念,用定义的接口类来标识,在上面的Person结构体定义的例子中可以看出有个group属性,该属性上有两个约束,分别是@NotNull(groups = Intf1.class) 和@Size(min = 1, max = 3, groups = Intf2.class)

下面通过一段代码执行的结果来看在参数校验中如何进行分组

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "group", null, Intf1.class); assertEquals(1, constraintViolations.size()); System.out.println("validate Intf1 |" + constraintViolations); constraintViolations = validator.validateValue(Person.class, "group",null, Intf2.class); assertEquals(0, constraintViolations.size()); System.out.println("validate Intf2 |" + constraintViolations); constraintViolations = validator.validateValue(Person.class, "group","test", Intf2.class, Intf1.class); assertEquals(1, constraintViolations.size()); System.out.println("validate Intf1&Intf2 |" + constraintViolations);

上段逻辑当group值为null时,首先用Intf1标识的约束条件进行校验,在用Intf2标识的约束条件进行校验。当group值为test时,同时用Intf1和Intf2标识的约束条件进行校验。执行的结果如下:

validate Intf1 | [ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=group, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.NotNull.message}'}]

validate Intf2 | []

validate Intf1&Intf2 | [ConstraintViolationImpl{interpolatedMessage='个数必须在1和3之间', propertyPath=group, rootBeanClass=class hibernate.validator.Person, messageTemplate='{javax.validation.constraints.Size.message}'}]

从上面的例子可以看出,可以根据具体的业务选择不同的校验规则。

(四)约束条件的定制

对约束条件的定制,需要两步,第一步是定义约束的注解类:

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = GenderTypeValidator.class) public @interface GenderCase { String message() default "genderType invalid"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; GenderType value() default GenderType.FEMALE; }

这里需要关注的有三个点,第一个是@Constraint(validatedBy = GenderTypeValidator.class) 这里指定了下面将要说的约束校验的实现类,第二个是message属性,用于校验值非法是缺省的消息模版,第三个是注解对应的约束值value。在这个例子中,注解的约束值用的是一个枚举值表示男/女,缺省值为女

第二步是实现了javax.validation.ConstraintValidator<A extends Annotation, T>接口的约束校验实现类,上面说的validatedBy指向的就是该实现类,其中A表示自定义的注解类,T表示进行校验的字段的类型。具体的逻辑定于如下:

public class GenderTypeValidator implements ConstraintValidator<GenderCase, GenderType> { GenderType value; @Override public void initialize(GenderCase constraintAnnotation) { value = constraintAnnotation.value(); } @Override public boolean isValid(GenderType obj, ConstraintValidatorContext context) { if (value != null && obj != null && value != obj) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate("gender should be " + value + "| the value is " + obj).addConstraintViolation(); return false; } else { return true; } } }

在初始化方法中获取该注解的约束条件,在isValid方法中将传进来的obj的值与约束条件比较,如果满足则返回true表示校验通过,如果不满足则返回false,并将错误信息存储上上下文ConstraintValidatorContext中,最终反馈给调用者。

下面是调用该定制约束条件的逻辑:

Set<ConstraintViolation<Person>> constraintViolations = validator.validateValue(Person.class, "gender", GenderType.MALE); assertEquals(1, constraintViolations.size()); System.out.println(constraintViolations);

执行的结果如下:

[ConstraintViolationImpl{interpolatedMessage='gender should be FEMALE| the value is MALE', propertyPath=gender, rootBeanClass=class hibernate.validator.Person, messageTemplate='gender should be FEMALE| the value is MALE'}]

继续阅读