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