Bean Validation

作者: BIN_1 | 来源:发表于2019-01-21 01:03 被阅读23次
    1、简介

    Bean Validation,是JCP(Java Community Process)定义的标准化的JavaBean校验API,基于注解,并且具有良好的易用性和扩展性;需要注意的是,Bean Validation只是一个规范和标准,并没有提供实现。

    2、Maven依赖
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    3、简单的使用
    • 注解方式
      Bean Validation默认是基于注解的,可以在三个字段级别约束、属性级别约束、类级别约束;访问级别(private, protected或者public)对此没有影响,但是如果字段是static的,则不会进行校验。
    // 字段约束
    @NotNull(message = "生产厂家不能为空")
    private String manufacturer;
    // 属性约束(如果字段也有约束的话,会重复)
    @NotEmpty(message = "出租车站不能为空")
    public String getRentalStation() {
        return rentalStation;
    }
    //类约束
    @PassengerCount(value = 2, message = "你确定要超载?")
    public class Car {
        ……
    }
    
    • 约束继承
      如果子类继承自他的父类,除了校验子类,同时还会校验父类,这就是约束继承,这同样适用于接口。如果子类覆盖了父类的方法,那么子类和父类的约束都会被校验。
    public class RentalCar extends Car {
        ……
    }
    @Test
    public void testRentalCar() {
        // 会校验子类,同时还会校验父类,这就是约束继承,这同样适用于接口。如果子类覆盖了父类的方法,那么子类和父类的约束都会被校验。
        RentalCar rentalCar = new RentalCar(null);
        Set<ConstraintViolation<RentalCar>> constraintViolations =
                validator.validate(rentalCar);
        assertEquals(4, constraintViolations.size());
    }
    
    • 约束级联
      如果要验证属性关联的对象,那么需要在属性上添加@Valid注解,如果一个对象被校验,那么它的所有的标注了@Valid的关联对象都会被校验,这些对象也可以是数组、集合、Map等,这时会验证他们持有的所有元素。
    @PassengerCount(value = 2, message = "你确定要超载?")
    public class Car {
        ……
    
        // valid注解会级联验证关联对象,如果对象是集合、Map、数组,也会验证其中的元素
        @Valid
        private Driver driver;
    
        @Valid
        private List<User> passengers;
    }
    
    • 约束校验
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    
    // 校验给定对象的所有约束
    <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
    // 校验对象的给定属性的所有约束
    <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups);
    // 使用给定的属性值来校验对象的给定属性上的所有约束
    <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups);
    
    // 校验所有约束
    @Test
    public void testRentalCar() {
        // 会校验子类,同时还会校验父类,这就是约束继承,这同样适用于接口。如果子类覆盖了父类的方法,那么子类和父类的约束都会被校验。
        RentalCar rentalCar = new RentalCar(null);
        Set<ConstraintViolation<RentalCar>> constraintViolations = validator.validate(rentalCar);
        assertEquals(4, constraintViolations.size());
    }
    // 校验属性约束
    @Test
    public void testPropertyValidate() {
        RentalCar rentalCar = new RentalCar(null);
        Set<ConstraintViolation<RentalCar>> constraintViolations = validator.validateProperty(rentalCar, "rentalStation");
        assertEquals(1, constraintViolations.size());
    }
    // 给定属性值,校验所有属性约束
    @Test
    public void testPropertyValueValidate() {
        Set<ConstraintViolation<RentalCar>> constraintViolations = validator.validateValue(RentalCar.class, "rentalStation", null);
        assertEquals(1, constraintViolations.size());
    }
    
    • ConstraintViolation
      该类用于描述违反约束的信息,通过它可以获得导致校验失败的消息。每个约束定义中都包含有一个用于提示验证结果的消息模版, 并且在声明一个约束条件的时候,可以通过这个约束中的message属性来重写默认的消息模版。如果在校验的时候,这个约束条件没有通过,那么配置的MessageInterpolator会被用来当成解析器来解析这个约束中定义的消息模版, 从而得到最终的验证失败提示信息。如果默认的消息解析器不能够满足需求,那么也可以在创建ValidatorFactory的时候, 将其替换为一个自定义的MessageInterpolator。
    // 获取违反约束的消息
    String getMessage();
    // 获取违反约束的消息模板,如{javax.validation.constraints.NotNull.message}
    String getMessageTemplate();
    // 获取被校验的根实体
    T getRootBean();
    // 获取被校验的根实体class
    Class<T> getRootBeanClass();
    // 如果约束是添加在一个实体对象上的,那么则返回这个对象的实例, 如果是约束是定义在一个属性上的, 则返回这个属性所属的实体对象.
    Object getLeafBean();
    // 获取从被验证的根对象到被验证的属性的路径.
    Path getPropertyPath();
    // 获取导致校验失败的值
    Object getInvalidValue();
    // 获取导致校验失败的约束定义
    ConstraintDescriptor<?> getConstraintDescriptor();
    
    • 校验组
    // 汽车检测组:
    public interface CarChecks {}
    // 驾驶员检测组:
    public interface DriverChecks {}
    
    @Data
    @PassengerCount(value = 2, message = "你确定要超载?")
    public class Car {
        // 在Car类上添加一个通过上路检测的属性,定义他所属的组为CarChecks:
        @AssertTrue(message = "必须先通过上路检测", groups = CarChecks.class)
        private boolean passedVehicleInspection;
    }
    
    @Data
    public class Driver {
        // 将属性加入DriverChecks组中
        @AssertTrue(message = "你想无证驾驶?", groups = DriverChecks.class)
        private boolean hasDrivingLicense;
    }
    
    // 现在可以使用组进行校验了,想要校验哪个组,就多传递一个组的参数。
    // 先创建一个Car,校验其默认组属性的约束,由于passedVehicleInspection属于CarChecks组,默认组并不校验;然后再校验CarChecks组,此时会校验组内的passedVehicleInspection属性;然后创建一个Driver,他并没有通过驾照考试,所以校验DriverChecks组时不能通过;最后,当司机也通过了驾照考试后,所有条件具备,当校验所有的组时,校验成功。
    @Test
    public void driveAway() {
        // 通过校验,默认组,由于passedVehicleInspection属于另外的组,所以这里并不校验该属性
        Car car = new Car("ford", "川A12345", 2);
        Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);
        assertEquals(0, constraintViolations.size());
        // 未通过上路检测
        constraintViolations = validator.validate(car, CarChecks.class);
        assertEquals(1, constraintViolations.size());
        assertEquals("必须先通过上路检测", constraintViolations.iterator().next().getMessage());
        // 设置通过上路检测
        car.setPassedVehicleInspection(true);
        assertEquals(0, validator.validate(car).size());
        // 设置一个司机
        Driver john = new Driver("john", "john@123.com", 20);
        car.setDriver(john);
        constraintViolations = validator.validate(car, DriverChecks.class);
        assertEquals(1, constraintViolations.size());
        assertEquals("你想无证驾驶?", constraintViolations.iterator().next().getMessage());
        // 司机通过了驾照考试
        john.setHasDrivingLicense(true);
        assertEquals(0, validator.validate(car, DriverChecks.class).size());
        // 全部通过
        assertEquals(0, validator.validate(car, Default.class, CarChecks.class, DriverChecks.class).size());
    }
    
    4、自定义校验
    • 创建约束注解
      message: 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过此属性来输出错误信息,通过该属性可以覆盖消息模板来自定义消息内容.
      groups,:用于指定这个约束条件属于哪(些)个校验组. 这个的默认值必须是Class<?>类型的数组.
      payload,:Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.
      Constraint注解:用来定义当前约束的校验器是什么
    @Documented
    @Constraint(validatedBy = PassengerCountValidator.class)
    @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface PassengerCount {
        String message() default "{com.belonk.car.passengerCount}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        int value();
    }
    
    • 实现校验器
      支持两个泛型参数:第一个为约束的注解,第二个为被校验目标对象,如果是类,则为类对象,如果是属性和方法,则为属性和方法的(返回)类型
    // 需要实现ConstraintValidator接口来自定义校验器:
    public interface ConstraintValidator<A extends Annotation, T> {
        // 初始化一些必要的信息
        default void initialize(A constraintAnnotation) {}
    
        // 校验逻辑,value为被校验目标(类、属性、方法)的值
        boolean isValid(T value, ConstraintValidatorContext context);
    }
    
    • 定义默认的校验错误信息
      sspath下新建一个ValidationMessages.properties文件,加入约束注解中定义的消息模板配置。
    com.belonk.car.passengerCount = 核载人数为{value},请勿超载
    
    5、约束组合

    有时候,在一个字段上加多个约束显得非常臃肿,不易阅读。我们可以通过组合这些约束,来自定义一个约束,从而简化编码,提高可读性

    • 自定义约束
    // 约束上添加需要组合的多个约束
    @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ValidNumber {
        String message() default "{com.belonk.car.no}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    
    // @ReportAsSingleViolation添加了这个注解后,表示不会验证所有约束,当有一个约束验证失败,则会立即返回组合约束所定义的错误信息(message属性定义),而不是单个约束本身的错误信息。
    //例如:如果@NotEmpty、@Pattern都校验失败,不添加@ReportAsSingleViolation注解,则会生成两个校验失败的结果,而提示信息为@NotEmpty、@Pattern对应的错误信息;相反,如果添加了@ReportAsSingleViolation注解,当@NotEmpty校验失败时直接返回校验错误,信息为@ValidNumber定义的错误信息。
    @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @NotEmpty
    @Size(min = 5, max = 16)
    @Pattern(regexp = "^GL.*$")
    // 不需要单独的验证器
    @Constraint(validatedBy = {})
    // 有约束校验失败立即返回,信息为组合约束定义的信息
    @ReportAsSingleViolation
    public @interface ValidNumber {
        String message() default "{com.belonk.car.no}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    

    相关文章

      网友评论

        本文标题:Bean Validation

        本文链接:https://www.haomeiwen.com/subject/wtcqjqtx.html