Hibernate Validator--创建自己的约束规则

尽管Bean Validation API定义了一大堆标准的约束条件, 但是肯定还是有这些约束不能满足我们需求的时候, 在这种情况下, 你可以根据你的特定的校验需求来创建自己的约束条件.

3.1. 创建一个简单的约束条件

按照以下三个步骤来创建一个自定义的约束条件

  • 创建约束标注

  • 实现一个验证器

  • 定义默认的验证错误信息

3.1.1. 约束标注

让我们来创建一个新的用来判断一个给定字符串是否全是大写或者小写字符的约束标注. 我们将稍后把它用在第 1 章 开始入门中的类CarlicensePlate字段上来确保这个字段的内容一直都是大写字母.

首先,我们需要一种方法来表示这两种模式( 译注: 大写或小写), 我们可以使用String常量, 但是在Java 5中, 枚举类型是个更好的选择:

例 3.1. 枚举类型CaseMode, 来表示大写或小写模式.

package com.mycompany;

public enum CaseMode {
    UPPER, 
    LOWER;
}

现在我们可以来定义真正的约束标注了. 如果你以前没有创建过标注(annotation)的话,那么这个可能看起来有点吓人, 可是其实没有那么难的 :)

例 3.2. 定义一个CheckCase的约束标注

package com.mycompany;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {

    String message() default "{com.mycompany.constraints.checkcase}";

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

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

}

一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法的样式的. 根据Bean Validation API 规范的要求

  • message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息.

  • groups 属性, 用于指定这个约束条件属于哪(些)个校验组(请参考第 2.3 节 “校验组”). 这个的默认值必须是Class<?>类型到空到数组.

  • payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.

    提示

    通过payload属性来指定默认错误严重级别的示例

    public class Severity {
        public static class Info extends Payload {};
        public static class Error extends Payload {};
    }
    
    public class ContactDetails {
        @NotNull(message="Name is mandatory", payload=Severity.Error.class)
        private String name;
    
        @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
        private String phoneNumber;
    
        // ...
    }

    这样, 在校验完一个ContactDetails 的示例之后, 你就可以通过调用ConstraintViolation.getConstraintDescriptor().getPayload()来得到之前指定到错误级别了,并且可以根据这个信息来决定接下来到行为.

除了这三个强制性要求的属性(message, groups 和 payload) 之外, 我们还添加了一个属性用来指定所要求到字符串模式. 此属性的名称value在annotation的定义中比较特殊, 如果只有这个属性被赋值了的话, 那么, 在使用此annotation到时候可以忽略此属性名称, 即@CheckCase(CaseMode.UPPER).

另外, 我们还给这个annotation标注了一些(所谓的) 元标注( 译注: 或"元模型信息"?, "meta annotatioins"):

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示@CheckCase 可以被用在方法, 字段或者annotation声明上.

  • @Retention(RUNTIME): 表示这个标注信息是在运行期通过反射被读取的.

  • @Constraint(validatedBy = CheckCaseValidator.class): 指明使用那个校验器(类) 去校验使用了此标注的元素.

  • @Documented: 表示在对使用了@CheckCase的类进行javadoc操作到时候, 这个标注会被添加到javadoc当中.

提示

Hibernate Validator provides support for the validation of method parameters using constraint annotations (see 第 8.3 节 “Method validation”).

In order to use a custom constraint for parameter validation the ElementType.PARAMETER must be specified within the @Target annotation. This is already the case for all constraints defined by the Bean Validation API and also the custom constraints provided by Hibernate Validator.

3.1.2. 约束校验器

Next, we need to implement a constraint validator, that's able to validate elements with a @CheckCase annotation. To do so, we implement the interface ConstraintValidator as shown below:

例 3.3. 约束条件CheckCase的验证器

package com.mycompany;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {

        if (object == null)
            return true;

        if (caseMode == CaseMode.UPPER)
            return object.equals(object.toUpperCase());
        else
            return object.equals(object.toLowerCase());
    }

}

ConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型(在我们的例子中即CheckCase), 第二个这个校验器所支持到被校验元素到类型 (即String).

如果一个约束标注支持多种类型到被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中.

这个验证器的实现就很平常了, initialize() 方法传进来一个所要验证的标注类型的实例, 在本例中, 我们通过此实例来获取其value属性的值,并将其保存为CaseMode类型的成员变量供下一步使用.

isValid()是实现真正的校验逻辑的地方, 判断一个给定的String对于@CheckCase这个约束条件来说是否是合法的, 同时这还要取决于在initialize()中获得的大小写模式. 根据Bean Validation中所推荐的做法, 我们认为null是合法的值. 如果null对于这个元素来说是不合法的话,那么它应该使用@NotNull来标注.

3.1.2.1. ConstraintValidatorContext

例 3.3 “约束条件CheckCase的验证器” 中的isValid使用了约束条件中定义的错误消息模板, 然后返回一个true 或者 false. 通过使用传入的ConstraintValidatorContext对象, 我们还可以给约束条件中定义的错误信息模板来添加额外的信息或者完全创建一个新的错误信息模板.

例 3.4. 使用ConstraintValidatorContext来自定义错误信息

package com.mycompany;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {

    private CaseMode caseMode;

    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    public boolean isValid(String object, ConstraintValidatorContext constraintContext) {

        if (object == null)
            return true;
        
        boolean isValid;
        if (caseMode == CaseMode.UPPER) {
            isValid = object.equals(object.toUpperCase());
        }
        else {
            isValid = object.equals(object.toLowerCase());
        }
        
        if(!isValid) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate( "{com.mycompany.constraints.CheckCase.message}"  ).addConstraintViolation();
        }
        return result;
    }

}

例 3.4 “使用ConstraintValidatorContext来自定义错误信息” 演示了如果创建一个新的错误信息模板来替换掉约束条件中定义的默认的. 在本例中, 实际上通过调用ConstraintValidatorContext达到了一个使用默认消息模板的效果.

提示

在创建新的constraint violation的时候一定要记得调用addConstraintViolation, 只有这样, 这个新的constraint violation才会被真正的创建.

In case you are implementing a ConstraintValidator a class level constraint it is also possible to adjust set the property path for the created constraint violations. This is important for the case where you validate multiple properties of the class or even traverse the object graph. A custom property path creation could look like 例 3.5 “Adding new ConstraintViolation with custom property path”.

例 3.5. Adding new ConstraintViolation with custom property path

public boolean isValid(Group group, ConstraintValidatorContext constraintValidatorContext) {
    boolean isValid = false;
    ...

    if(!isValid) {
        constraintValidatorContext
            .buildConstraintViolationWithTemplate( "{my.custom.template}" )
            .addNode( "myProperty" ).addConstraintViolation();
    }
    return isValid;
}

3.1.3. 校验错误信息

最后, 我们还需要指定如果@CheckCase这个约束条件验证的时候,没有通过的话的校验错误信息. 我们可以添加下面的内容到我们项目自定义的ValidationMessages.properties (参考 第 2.2.4 节 “验证失败提示信息解析”)文件中.

例 3.6. 为CheckCase约束定义一个错误信息

com.mycompany.constraints.CheckCase.message=Case mode must be {value}.

如果发现校验错误了的话, 你所使用的Bean Validation的实现会用我们定义在@CheckCase中message属性上的值作为键到这个文件中去查找对应的错误信息.

3.1.4. 应用约束条件

现在我们已经有了一个自定义的约束条件了, 我们可以把它用在第 1 章 开始入门中的Car类上, 来校验此类的licensePlate属性的值是否全都是大写字母.

例 3.7. 应用CheckCase约束条件

package com.mycompany;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    @CheckCase(CaseMode.UPPER)
    private String licensePlate;

    @Min(2)
    private int seatCount;
    
    public Car(String manufacturer, String licencePlate, int seatCount) {

        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...

}

最后,让我们用一个简单的测试来检测@CheckCase约束已经被正确的校验了:

例 3.8. 演示CheckCase的验证过程

package com.mycompany;

import static org.junit.Assert.*;

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.junit.BeforeClass;
import org.junit.Test;

public class CarTest {

    private static Validator validator;

    @BeforeClass
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    public void testLicensePlateNotUpperCase() {

        Car car = new Car("Morris", "dd-ab-123", 4);

        Set<ConstraintViolation<Car>> constraintViolations =
            validator.validate(car);
        assertEquals(1, constraintViolations.size());
        assertEquals(
            "Case mode must be UPPER.", 
            constraintViolations.iterator().next().getMessage());
    }

    @Test
    public void carIsValid() {

        Car car = new Car("Morris", "DD-AB-123", 4);

        Set<ConstraintViolation<Car>> constraintViolations =
            validator.validate(car);

        assertEquals(0, constraintViolations.size());
    }
}

3.2. 约束条件组合

例 3.7 “应用CheckCase约束条件”中我们可以看到, 类CarlicensePlate属性上定义了三个约束条件. 在某些复杂的场景中, 可能还会有更多的约束条件被定义到同一个元素上面, 这可能会让代码看起来有些复杂, 另外, 如果在另外的类里面还有一个licensePlate属性, 我们可能还要把这些约束条件再拷贝到这个属性上, 但是这样做又违反了 DRY 原则.

这个问题可以通过使用组合约束条件来解决. 接下来让我们来创建一个新的约束标注@ValidLicensePlate, 它组合了@NotNull, @Size@CheckCase:

例 3.9. 创建一个约束条件组合ValidLicensePlate

package com.mycompany;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface ValidLicensePlate {

    String message() default "{com.mycompany.constraints.validlicenseplate}";

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

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

}

 

我们只需要把要组合的约束标注在这个新的类型上加以声明 (注: 这正是我们为什么把annotation types作为了@CheckCase的一个target). 因为这个组合不需要额外的校验器, 所以不需要声明validator属性.

现在, 在licensePlate属性上使用这个新定义的"约束条件" (其实是个组合) 和之前在其上声明那三个约束条件是一样的效果了.

例 3.10. 使用ValidLicensePlate组合约束

package com.mycompany;

public class Car {

    @ValidLicensePlate
    private String licensePlate;

    //...

}

 

The set of ConstraintViolations retrieved when validating a Car instance will contain an entry for each violated composing constraint of the @ValidLicensePlate constraint. If you rather prefer a single ConstraintViolation in case any of the composing constraints is violated, the @ReportAsSingleViolation meta constraint can be used as follows:

例 3.11. @ReportAsSingleViolation的用法

//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {

    String message() default "{com.mycompany.constraints.validlicenseplate}";

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

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

}
posted on 2015-12-03 21:32  duanxz  阅读(2934)  评论(0编辑  收藏  举报