Bean Validation
2019-01-21 本文已影响23人
BIN_1
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 {};
}