从0到1熟悉HibernateValidate——增长你的技术肌肉

简介

Hibernate Validator 是一个JSR规则的实现。

依赖

text 复制代码
// gradle
plugins {
    id 'java'
}
java {
    version = JavaVersion.VERSION_17
}
group = 'cn.mj'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.hibernate.validator:hibernate-validator:8.0.1.Final")
    implementation('org.glassfish.expressly:expressly:5.0.0') // el表达式处理,用于处理动态计算和message处理
    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

基本使用

以下是基本的使用:

  1. Validator是线程安全的,定义一个静态变量保存起来,不要每次都创建,可以多次使用
  2. 通过Validation.buildDefaultValidatorFactory()创建一个ValidatorFactory,然后调用getValidator()方法获取一个Validator
  3. 创建一个Person对象(标注了Constraint)
  4. 通过validator.validate(object)方法校验
  5. ConstraintViolation即验证的结果,如果4返回空那么验证没问题,否则有问题
java 复制代码
class HibernateValidateTest {
    private static Validator validator;
    @BeforeAll
    public static void setUp() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    @Test
    public void test() {
        Person person = new Person("", 101);
        Set<ConstraintViolation<Person>> violationSet = validator.validate(person);
        for (ConstraintViolation<Person> violation : violationSet) {
            System.out.println(violation.getMessage());
        }
    }
}
public class Person {
    @NotBlank
    @Size(max = 10)
    private String name;
    @NotNull
    @Min(0)
    @Max(100)
    private Integer age;
}

声明、验证bean的Constraint

  1. 先在Bean上声明Constraint 2. 验证bean实例是否满足Constraint Constraint即JSR规定的那些验证annotation、自定义的 constraint annotation

声明Bean Constraint

Constraint支持4中类型的标注:

  1. 字段
  2. 属性
  3. 容器元素

字段即平时定义的哪些入参,字段类型为private,提供getter、setter方法;属性即对于标准的Java Bean实体类来说,属性通过构造函数初始化,后面只能通过getter方法获取、业务方法修改,而不是setter方法修改(领域驱动的业务实体)。

字段级别

通过字节码增强技术(反射reflection)直接访问字段,而不通过getter、setter方法。支持public、protected、default、private访问类型,不支持static类型。

java 复制代码
public class Person {
    @NotBlank
    @Size(max = 10)
    private String name;
    @NotNull
    @Min(0)
    @Max(100)
    private Integer age;
    // getter、setter
}

属性级别

如果是标准的JavaBean,只能通过getter方法访问属性。

java 复制代码
public class Order {
    private Long orderId;
    private String status;

    public Order(Long orderId, String status) {
        this.orderId = orderId;
        this.status = status;
    }
    @NotNull
    public Long getOrderId() {
        return orderId;
    }
    @NotBlank
    public String getStatus() {
        return status;
    }
}

注意:不要同时在字段、getter上都添加Constraint注解,否则会校验两次。

容器元素级别

Constraint annotation可以标注到支持泛型的容器的泛型实参上。支持:Map、Optional、OptionalInt、OptionalLong、OptionalDouble、实现了Iterable的(List、Set)。 自定义这类annotation,需要在Target上指定ElementType.TYPE_USE。 在6之前,需要使用@Valid注解,之后不需要

Iterable

可以支持对List、Set的泛型实参添加Constraint标注。

java 复制代码
public class Dog {
    private final Set<@NotBlank String> names = new HashSet<>();
    public void addName(String name){
        names.add(name);
    }
    public Set<String> getNames(){
        return names;
    }
}
Map

可以对Map的Key、Value实参进行标注。

java 复制代码
public class Student {
    private final Map<@NotBlank String,@NotNull @Min(0) @Max(100) Float> scores = new HashMap<>();
    public void addScore(String subject,Float score){
        scores.put(subject,score);
    }

    public Map<@NotBlank String, @NotNull @Min(0) @Max(100) Float> getScores() {
        return scores;
    }
}
public class Test{
    @Test
    public void mapTest(){
        var stu = new Student();
        stu.addScore("math",100.0F);
        stu.addScore("chinese",101.0F);
        stu.addScore("",101.0F);
        Set<ConstraintViolation<Student>> violationSet = validator.validate(stu);
        for (ConstraintViolation<Student> violation : violationSet) {
            System.out.println(violation.getMessage());
        }
    }
}
Optional

Hibernate Validator会自动unwrap Optional,直接验证internal value。

java 复制代码
public class Stock {
    private Optional<@Min(0) Float> money = Optional.empty();
    public Optional<Float> getMoney() {
        return money;
    }
    public void setMoney(@NotNull Float m) {
        money = Optional.of(m);
    }
}
public class Test{
    @Test
    public void optionalTest(){
        var sock = new Stock();
        sock.setMoney(2F);
        sock.setMoney(-1F);
        Set<ConstraintViolation<Stock>> violationSet = validator.validate(sock);
        for (ConstraintViolation<Stock> violation : violationSet) {
            System.out.println(violation.getMessage());
        }
    }
}
自定义容器类型

需要实现ValueExtractor提取值

java 复制代码
public class Car {
 private GearBox<@MinTorque(100) Gear> gearBox;
 public void setGearBox(GearBox<Gear> gearBox) {
 this.gearBox = gearBox;
 }
 //...
}
public class GearBox<T extends Gear> {
 private final T gear;
 public GearBox(T gear) {
 this.gear = gear;
 }
 public Gear getGear() {
 return this.gear;
 }
}
public class Gear {
 private final Integer torque;
 public Gear(Integer torque) {
 this.torque = torque;
 }
 public Integer getTorque() {
 return torque;
 }
 public static class AcmeGear extends Gear {
 public AcmeGear() {
 super( 60 );
 }
 }
}
public class GearBoxValueExtractor implements ValueExtractor<GearBox<@ExtractedValue ?>> {
 @Override
 public void extractValues(GearBox<@ExtractedValue ?> originalValue, ValueExtractor
           .ValueReceiver receiver) {
 receiver.value( null, originalValue.getGear() );
 }
}
嵌套容器元素

支持嵌套容器元素,例如List<Map<String,String>>。

java 复制代码
public class Car {
 private Map<@NotNull Part, List<@NotNull Manufacturer>> partManufacturers = new HashMap<>();
 //...
}

类级别

Constraint可以标注在类上,此时Constraint会作用到该类的整个对象验证,可以实现多个属性有依赖关系的这种验证。 比如:一个Car里有seatCount座位数,那么Car里的passengers的个数就需要小于seatCount。

java 复制代码
@ValidPassengerCount
public class Car {
 private int seatCount;
 private List<Person> passengers;
 //...
}

Constraint继承

当一个class实现了interface或者class,所有在父级标注的Constraint都会被应用到子类,对于实现接口同理。 如下例子:Car对应manufacturer属性标注了NotNull,同时子类RentalCar新增属性rentalStation标注了NotNull,那么对于RentalCar来说,manufacturer和rentalStation都会被校验。

java 复制代码
public class Car {
 private String manufacturer;
 @NotNull
 public String getManufacturer() {
 return manufacturer;
 }
 //...
}
public class RentalCar extends Car {
 private String rentalStation;
 @NotNull
 public String getRentalStation() {
 return rentalStation;
 }
 //...
}

级联验证

嵌套对象,嵌套对象集合,嵌套对象数组等场景下,需要使用@Valid注解,否则嵌套对象不会被校验。 如下例子:当Car的driver属性为null,不会校验driver,否则就会校验driver。

java 复制代码
public class Car {
 @NotNull
 @Valid
 private Person driver;
 //...
}
public class Person {
 @NotNull
 private String name;
 //...
}

级联验证也可以作用于容器,不过需要给容器的元素类型标注@Valid注解。

在Hibernate Validator 6.0.0.Final之前,需要@Valid private List<Person>如此进行标注。

java 复制代码
public class Car {
 private List<@NotNull @Valid Person> passengers = new ArrayList<Person>();
 private Map<@Valid Part, List<@Valid Manufacturer>> partManufacturers = new HashMap<>();
 //...
}
public class Part {
 @NotNull
 private String name;
 //...
}
public class Manufacturer {
 @NotNull
 private String name;
 //...
}

内置的Constraint Annotation

内置的Constraint Annotation只能验证property、field,没有class级别的,需要自己实现。

  1. @AssertFalse 支持Boolean, boolean
  2. @AssertTrue 支持Boolean, boolean
  3. @DecimalMax(value=, inclusive=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
  4. @DecimalMin(value=, inclusive=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
  5. @Digits(integer=, fraction=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
  6. @Email 支持CharSequence
  7. @Future 支持java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate。高版本支持joda-time
  8. @FutureOrPresent 支持java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate。高版本支持joda-time
  9. @Max(value=) 支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  10. @Min(value=) 支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  11. @NotBlank 支持CharSequence。验证字符串是否为空,空字符串,空格,null
  12. @NotEmpty 支持CharSequence, Collection, Map and arrays。验证是否为null或者长度为0
  13. @NotNull 支持任何类型
  14. @Negative 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  15. @NegativeOrZero 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  16. @Null 支持任何类型
  17. @Past 支持java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate。高版本支持joda-time
  18. @PastOrPresent 支持java.util.Date, java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate。高版本支持joda-time
  19. @Pattern(regex=, flags=) 支持CharSequence
  20. @Positive 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  21. @PositiveOrZero 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
  22. @Size(min=, max=) 支持CharSequence, Collection, Map and arrays
  23. @CreditCardNumber(ignoreNonDigitCharacters=) 支持CharSequence
  24. @Currency(value=) 支持 javax.money.MonetaryAmount 子类
  25. @DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) 支持java.time.Duration
  26. @DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) 支持java.time.Duration
  27. @ISBN (type=) 支持CharSequence
  28. @Length(min=, max=) 支持CharSequence
  29. @Range(min=, max=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long
  30. @UniqueElements 支持Collection子类
  31. @URL(protocol=, host=, port=, regexp=, flags=) 支持CharSequence
  32. @UUID(allowEmpty=, allowNil=, version=, variant=, letterCase=) 支持CharSequence

声明、验证方法Constraint

Hibernate Validator不仅支持JavaBean、属性的验证,还支持方法级别的验证。 在方法执行执行前置验证入参parameter,后置return value验证返回值。

声明方法验证

参数验证

对于成员方法、构造函数可以声明入参的验证。注意不支持static方法的验证。 如果不依赖第三方框架需要使用ExecutableValidator进行验证,否则第三方框架可以通过AOP、proxy 进行实现。

java 复制代码
public class RentalStation {
 public RentalStation(@NotNull String name) {
 //...
 }
 public void rentCar(
 @NotNull Customer customer,
 @NotNull @Future Date startDate,
 @Min(1) int durationInDays) {
 //...
 }
}
跨参数验证

有些时候需要同时验证方法的多个参数,而不是单个参数的验证。 与单个参数的验证不同,多个参数的验证是添加annotation到方法、构造函数上。 另外,返回值的校验也是标注annotation到方法上。 比如:

java 复制代码
public class Car {
 @LuggageCountMatchesPassengerCount(piecesOfLuggagePerPassenger = 2)
 public void load(List<Person> passengers, List<PieceOfLuggage> luggage) {
 //...
 }
}

为了区分标注到方法上的Constraint Annotation是验证返回值还是入参,需要在实现ConstraintValidator的类上标注@SupportedValidationTarget Annotation。 某些情况下也支持方法上标注的Constraint Annotation同时验证入参、返回值。 有些Constraint在声明的时候必须指定validationAppliesTo()来指定目标。validationAppliesTo = ConstraintTarget.PARAMETERS为验证入参,ConstraintTarget.RETURN_VALUE为验证返回值。

java 复制代码
public class Garage {
 @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.PARAMETERS)
 public Car buildCar(List<Part> parts) {
 //...
 return null;
 }
 @ELAssert(expression = "...", validationAppliesTo = ConstraintTarget.RETURN_VALUE)
 public Car paintCar(int color) {
 //...
 return null;
 }
}

返回值验证

如果需要后置验证一个方法、构造函数,需要添加Constraint Annotation到可执行的方法、构造函数上。

java 复制代码
public class RentalStation {
 @ValidRentalStation
 public RentalStation() {
 //...
 }
 @NotNull
 @Size(min = 1)
 public List<@NotNull Customer> getCustomers() {
 //...
 return null;
 }
}

级联验证

当方法参数、返回值验证标注了@Valid时,会进行级联验证。

java 复制代码
public class Garage {
 @NotNull
 private String name;
 @Valid
 public Garage(String name) {
 this.name = name;
 }
 public boolean checkCar(@Valid @NotNull Car car) {
 //...
 return false;
 }
 public boolean checkCars(@NotNull List<@Valid Car> cars) {
 //...
 return false;
 }
}

public class Car {
 @NotNull
 private String manufacturer;
 @NotNull
 @Size(min = 2, max = 14)
 private String licensePlate;
 public Car(String manufacturer, String licencePlate) {
 this.manufacturer = manufacturer;
 this.licensePlate = licencePlate;
 }
 //getters and setters ...
}

方法验证可以被继承

当需要使用、扩展 方法验证继承的时候,需要满足一下条件:

  1. 前置验证的条件:父类必须比子类严格(父类设置参数验证,子类重写时不需要指定,使用父类的)
  2. 后置验证的条件:父类必须比子类严格(父类不设置、设置宽松,子类设置、设置严格,使用子类的)

如下为一个不合法的例子。正常情况:父类、接口设置了参数验证,子类就不允许disallow重新设置参数验证注解了。

java 复制代码
public interface Vehicle {
 void drive(@Max(75) int speedInMph);
}
public class Car implements Vehicle {
 @Override
 public void drive(@Max(55) int speedInMph) {
 //...
 }
}

如果存在两个同一级的多个接口或者类,互相不干扰,都有同一个抽象方法待实现(有些标注看Constraint Annotation,有些没有),那么此时如果一个子类同时实现了所有的,此时是不合法的。 这种情况下父类都不应该设置参数验证。 如下为错误例子:

java 复制代码
public interface Vehicle {
 void drive(@Max(75) int speedInMph);
}
public interface Car {
 void drive(int speedInMph);
}
public class RacingCar implements Car, Vehicle {
 @Override
 public void drive(int speedInMph) {
 //...
 }
}

对于返回值校验(后置校验)在实现、重写方法的时候可以标注Constraint Annotation,此时所有返回值的校验都会使用子类标注的Constraint Annotation。 正常case如下:

java 复制代码
public interface Vehicle {
 @NotNull
 List<Person> getPassengers();
}
public class Car implements Vehicle {
 @Override
 @Size(min = 1)
 public List<Person> getPassengers() {
 //...
 return null;
 }
}

对于构造器而言,入参、返回值的验证值生效与当前类标注的,与父类、子类无关。

异常信息处理

Constraint Annotation在定义的时候可以定义默认的错误信息,在声明注解的时候也可以指定message覆盖默认的message。 当验证失败时,MessageInterpolator会通过validate engine 产品处理错误信息。 错误信息可以在验证结果里通过ConstraintViolation#getMessage()进行获取。 message可以包含表达式、参数 ,表达式会在validate engine处理错误信息的时候进行填充。

默认消息填充

消息变量parameter通过使用"{}"设置,表达式expression使用"${}"设置。 在处理消息、填充变量、表达式时有如下算法:

  1. 解析消息变量,将变量名作为key在classpath下的ValidationMessages.properties文件中查找。支持多语言,多语言命名样式ValidationMessages_en_US.properties。通过读取jvm的Locale#getDefault()获取语言
  2. 同1,只不过在 Appendix B 里进行查找。如果找到了就会再次执行1,否则执行3
  3. 解析消息变量,使用Constraint Annotation里的member进行填充。比如:"must be at least ${min}" 注解为:Size#min()
  4. 解析消息表达式,将他们作为el表达式进行计算

特殊符号

因为表达式、变量中使用的{、}、$ 有特殊含义,因此有些符号需要进行转义。

  1. 如果要使用{ 则需要 {
  2. 如果要使用} 则需要 }
  3. 如果要使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> 则需要 则需要 </math>则需要
  4. 如果要使用\ 则需要 \

填充消息表达式

在Hibernate Validate 5,支持使用Jakarta Expression Language 在message里。 这样可以在表达式里存在一些条件逻辑、格式化等。 Validate Engine规定了在EL表达式里支持一下对象:

  1. constraint annotation对应的参数名称。比如{min}, {max}
  2. 当前被验证的值,通过validatedValue这个key标识获取
  3. 对应的格式化器(要求Class名和变量名相同(首字母不一样),同时有对应的类似format的方法),例如:java.util.Formatter.format(String format, Object... args).

举例

java 复制代码
public class Car {
 @NotNull
 private String manufacturer;
 @Size(
 min = 2,
 max = 14,
 message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
 )
 private String licensePlate;
 @Min(
 value = 2,
 message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
 )
 private int seatCount;
 @DecimalMax(
 value = "350",
 message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
 "than {value}"
 )
 private double topSpeed;
 @DecimalMax(value = "100000", message = "Price must not be higher than ${value}")
 private BigDecimal price;
 public Car(
 String manufacturer,
 String licensePlate,
 int seatCount,
 double topSpeed,
 BigDecimal price) {
 this.manufacturer = manufacturer;
 this.licensePlate = licensePlate;
 this.seatCount = seatCount;
 this.topSpeed = topSpeed;
 this.price = price;
 }
 //getters and setters ...
}

自定义message 处理

如果默认message处理不能满足,可以自定义message处理,通过实现MessageInterpolator接口。注意:实现必须线程安全的。 推荐在实现的时候代理使用默认的消息处理,通过Configuration#getDefaultMessageInterpolator()进行获取默认的。 如果要使用自定义message处理需要在META-INF/validation.xml进行配置或者在Validator、ValidatorFactory进行配置。

Group 验证

Constraint Annotation可以设置group,通过group进行分组,在验证的时候可以指定group,通过group进行过滤。注意:如果需要在使用Spring Web需要验证使,需要使用Spring的@Validated代替@Valid设置分组,@Valid不支持设置分组。

使用Group

Group允许在验证的时候限制Constraint的集合,不同情况使用不同的Constraint进行验证。 如果没有指定group,默认使用jakarta.validation.groups.Default 。 当超过一个Group请求的时候,验证时使用的不同Group对应的Constraint的顺序是随机的。 默认使用的Default Group。

java 复制代码
public class Person {
 @NotNull
 private String name;
 public Person(String name) {
 this.name = name;
 }
 // getters and setters ...
}

指定Group。

通过声明一个interface来声明一个Group(类型安全、可以简单重构),也就意味着Group是可以支持继承的。

java 复制代码
public class Driver extends Person {
 @Min(
 value = 18,
 message = "You have to be 18 to drive a car",
 groups = DriverChecks.class
 )
 public int age;
 @AssertTrue(
 message = "You first have to pass the driving test",
 groups = DriverChecks.class
 )
 public boolean hasDrivingLicense;
 public Driver(String name) {
 super( name );
 }
 public void passedDrivingTest(boolean b) {
 hasDrivingLicense = b;
 }
 public int getAge() {
 return age;
 }
 public void setAge(int age) {
 this.age = age;
 }
}
public interface DriverChecks {
}

多个不同的Constraint使用不同的Group.

java 复制代码
public class Car {
 @NotNull
 private String manufacturer;
 @NotNull
 @Size(min = 2, max = 14)
 private String licensePlate;
 @Min(2)
 private int seatCount;
 @AssertTrue(
 message = "The car has to pass the vehicle inspection first",
 groups = CarChecks.class
 )
 private boolean passedVehicleInspection;
 @Valid
 private Driver driver;
 public Car(String manufacturer, String licencePlate, int seatCount) {
 this.manufacturer = manufacturer;
 this.licensePlate = licencePlate;
 this.seatCount = seatCount;
 }
 public boolean isPassedVehicleInspection() {
 return passedVehicleInspection;
 }
 public void setPassedVehicleInspection(boolean passedVehicleInspection) {
 this.passedVehicleInspection = passedVehicleInspection;
 }
 public Driver getDriver() {
 return driver;
 }
 public void setDriver(Driver driver) {
 this.driver = driver;
 }
 // getters and setters ...
}

上面例子使用三个不同的Group:Default、CarChecks、DriverChecks。 以下展示了如何使用Hibernate Validate进行分组校验,主要在Validator#validate() 方法传入分组。

java 复制代码
// create a car and check that everything is ok with it.
Car car = new Car( "Morris", "DD-AB-123", 2 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );
// but has it passed the vehicle inspection?
constraintViolations = validator.validate( car, CarChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
 "The car has to pass the vehicle inspection first",constraintViolations.iterator().next().getMessage());
// let's go to the vehicle inspection
car.setPassedVehicleInspection( true );
assertEquals( 0, validator.validate( car, CarChecks.class ).size() );
// now let's add a driver. He is 18, but has not passed the driving test yet
Driver john = new Driver( "John Doe" );
john.setAge( 18 );
car.setDriver( john );
constraintViolations = validator.validate( car, DriverChecks.class );
assertEquals( 1, constraintViolations.size() );
assertEquals(
 "You first have to pass the driving test",
 constraintViolations.iterator().next().getMessage()
);
// ok, John passes the test
john.passedDrivingTest( true );
assertEquals( 0, validator.validate( car, DriverChecks.class ).size() );
// just checking that everything is in order now
assertEquals(
 0, validator.validate(
 car,
 Default.class,
 CarChecks.class,
 DriverChecks.class
).size()
);

Group继承

通过使用一个Group继承多个Group即可实现一个Group包含多个Group的功能,无需指定多个Group。

java 复制代码
public class SuperCar extends Car {
 @AssertTrue(
 message = "Race car must have a safety belt",
 groups = RaceCarChecks.class
 )
 private boolean safetyBelt;
 // getters and setters ...
}
import jakarta.validation.groups.Default;
public interface RaceCarChecks extends Default {
}

定义Group集

默认情况下多个Group之间的验证是无序的,但是有些时候需要依赖Group的顺序进行验证。 可以通过创建一个interface并标注@GroupSequence,用来标注多个Group校验的顺序。一单验证失败了就不会继续验证后面的Group了。注意不能出现循环依赖,否则抛出GroupDefinitionException。 如下定义了一个Group Sequence。

java 复制代码
import jakarta.validation.GroupSequence;
import jakarta.validation.groups.Default;
@GroupSequence({ Default.class, CarChecks.class, DriverChecks.class })
public interface OrderedChecks {
}

重定义默认分组

分组转换

创建自定义的Constraint

有些时候默认自带的Constraint不能满足需求,此时可以自定义Constraint。

创建简单的Constraint

下面将举例实现一个Constraint用于验证一个字符串是不是完全的大写、小写。

创建Constraint Annotation

定义一个枚举类,用于定义两种不同的验证模式。

java 复制代码
public enum CaseMode {
 UPPER,
 LOWER;
}

接下来就是定义一个Constraint Annotation。

java 复制代码
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {
 String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase.message}";
 Class<?>[] groups() default { };
 Class<? extends Payload>[] payload() default { };
 CaseMode value();
 @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
 @Retention(RUNTIME)
 @Documented
 @interface List {
 CheckCase[] value();
 }
}

通过使用@interface定义一个注解,在注解里面属性通过方法的形式定义。 在定义Constraint Annotation的时候需要满足以下Jakarta Bean Validation API规范(需要包含三个属性):

  1. message 定义验证失败的提示信息
  2. groups 定义可以验证的分组,默认为一个空数组
  3. payload 定义在使用Jakarta Bean Validation API可以赋予的一些上下文什么的。 具体实现方式如下:
java 复制代码
public class Severity {
 public interface Info extends Payload {
 }
 public interface 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()获取Severity来判断是Info还是Error以及调整一些逻辑。

除了以上三个必不可少的属性attribute之外,还可以添加自定义的属性。上面设置的value用于指定具体以哪种方式进行验证。 此外,Constraint Annotation还绑定了一系列元注解:

  1. @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE}) 指定了注解支持的元素类型。FIELD表示属性,METHOD表示方法返回值, PARAMETER表示方法、构造函数参数参数,ANNOTATION_TYPE表示可以标注到注解上进行组合,TYPE_USE表示可以标注到泛型实参上。当需要标注到类级别是需要设置为TYPE。 当验证的目标为构造函数的返回值=时需要设置为CONSTRUCTOR,当需要使用跨多个参数验证时需要设置METHOD or CONSTRUCTOR。
  2. @Retention(RUNTIME) 指定了在运行时可以通过反射获取到
  3. @Constraint(validatedBy = CheckCaseValidator.class): 指定在验证时使用的Constraint Validator,同时对于不同的数据类型可以设置多个不同的Constraint Validator。
  4. @Documented: 指定了在JavaDoc上进行展示
  5. @Repeatable(List.class) 指定了Constraint Annotation在一个地方可以标注多次。

实现Validator

当定义了一个Constraint Annotation以后,需要实现一个ConstraintValidator,用来完成Constraint Annotation的验证。

java 复制代码
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
     private CaseMode caseMode;
     @Override
     public void initialize(CheckCase constraintAnnotation) {
     this.caseMode = constraintAnnotation.value();
     }
     @Override
     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有两个泛型参数需要在实现的时候指定,第一个是自定义的Constraint Annotation,第二个是验证的目标的类型。如果需要支持多个类型,需要多个ConstraintValidator的实现。 initialize()允许获取在标注Constraint Annotation时设置的属性值,可以保存到成员变量里。以便在后面的验证里使用。该方法只会调用一次。 isValid() 实现具体的验证逻辑,会执行多次。注意:推荐把null视为有效,如果不能接受null就标注@NotNull。

使用ConstraintValidatorContext

在使用的时候也可以使用表达式,此时需要实现HibernateConstraintValidatorContext。 另外,在验证中有时需要另外的异常信息而不是使用默认的异常信息。如下案例:

java 复制代码
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
 private CaseMode caseMode;
 @Override
 public void initialize(CheckCase constraintAnnotation) {
 this.caseMode = constraintAnnotation.value();
 }
 @Override
 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(
 "{org.hibernate.validator.referenceguide.chapter06." +
 "constraintvalidatorcontext.CheckCase.message}"
 )
 .addConstraintViolation();
 }
 return isValid;
 }
}

通过使用ConstraintValidatorContext::disableDefaultConstraintViolation禁用默认的异常信息, 使用ConstraintValidatorContext::buildConstraintViolationWithTemplate设置新的异常信息,然后通过addConstraintViolation进行添加。

定义默认的异常信息

定义异常信息有两种方式:1. 代码写死;2. 通过读取配置文件ValidationMessages.properties,支持多语言。 最前面我们定义注解时指定了取异常信息的key: String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase.message}";。 在配置文件进行配置:

properties 复制代码
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.

使用Constraint Validator

有几种方式,直接使用Hibernate Validator进行验证,使用框架验证(Spring)。 先定义一个待验证的JavaBean。

java 复制代码
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 ...
}

第一种使用:

java 复制代码
//invalid license plate
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()
);
//valid license plate
car = new Car( "Morris", "DD-AB-123", 4 );
constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );

使用框架: Spring这种在Web模块有实现Validate相关功能,同时会加载ClassPath上引入的Hibernate Validate。

类级别验证

类级别的验证和属性的验证差不多,需要设置一个@Target元注解到TYPE。

先验证Bean单个属性的Constraint Annotation,然后再验证整个类级别的。 如下:

java 复制代码
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { ValidPassengerCountValidator.class })
@Documented
public @interface ValidPassengerCount {
 String message() default " {org.hibernate.validator.referenceguide.chapter06.classlevel.ValidPassengerCount.message}";
 Class<?>[] groups() default { };
 Class<? extends Payload>[] payload() default { };
}
public class ValidPassengerCountValidator
 implements ConstraintValidator<ValidPassengerCount, Car> {
 @Override
 public void initialize(ValidPassengerCount constraintAnnotation) {
 }
 @Override
 public boolean isValid(Car car, ConstraintValidatorContext context) {
 if ( car == null ) {
 return true;
 }
 return car.getPassengers().size() <= car.getSeatCount();
 }
}

跨参数验证

定义跨参数验证,可以同时验证多个入参,注意:需要在Validator的实现上标注@SupportedValidationTarget(ValidationTarget.PARAMETERS)标识验证的参数,以及需要将值的类型设置为Object或者Object[]。 注意: Constraint Annotation的Constraint注解可以配置多个Validator。因此可以根据不同的值的类型实现定义多个Validator。 如下:

java 复制代码
@Constraint(validatedBy = ConsistentDateParametersValidator.class)
@Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {
 String message() default "{org.hibernate.validator.referenceguide.chapter04.crossparameter.ConsistentDateParameters.message}";
 Class<?>[] groups() default { };
 Class<? extends Payload>[] payload() default { };
}

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParametersValidator implements
 ConstraintValidator<ConsistentDateParameters, Object[]> {
         @Override
 public void initialize(ConsistentDateParameters constraintAnnotation) {
 }
 @Override
 public boolean isValid(Object[] value, ConstraintValidatorContext context) {
 if ( value.length != 2 ) {
 throw new IllegalArgumentException( "Illegal method signature" );
 }
 //leave null-checking to @NotNull on individual parameters
 if ( value[0] == null || value[1] == null ) {
 return true;
 }
 if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) {
 throw new IllegalArgumentException(
                 "Illegal method signature, expected two " +
                 "parameters of type Date."
                 );
 }
 return ( (Date) value[0] ).before( (Date) value[1] );
 }
}
相关推荐
jonyleek12 分钟前
数据可视化:JVS-BI仪表盘图表样式配置全攻略,打造个性化数据展示!
java·大数据·信息可视化·数据挖掘·数据分析·自动化·软件需求
WangMing_X12 分钟前
C# 单个函数实现各进制数间转换
java·开发语言·算法·c#·winform·软件
天人合一peng19 分钟前
20201010 MTAP-3DGAM审稿意见
后端·3d·restful
南宫生25 分钟前
贪心算法理论基础和习题【算法学习day.17】
java·学习·算法·leetcode·链表·贪心算法
jc0803kevin31 分钟前
solidity的struct对象,web3j java解析输出参数
java·web3·solidity
勇敢滴勇34 分钟前
【C++】继承和多态常见的面试问题
java·c++·面试
nice6666039 分钟前
初识JDBC
java·数据库·sql·mysql·idea
计算机学姐41 分钟前
基于SpringBoot的汽车票网上预订系统
java·vue.js·spring boot·后端·mysql·java-ee·mybatis
screamn1 小时前
Sentinel详解
java·sentinel
哎呦没1 小时前
农村扶贫管理:SpringBoot解决方案
java·spring boot·后端