从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] );
 }
}
相关推荐
Asthenia04121 小时前
Spring扩展点与工具类获取容器Bean-基于ApplicationContextAware实现非IOC容器中调用IOC的Bean
后端
bobz9651 小时前
ovs patch port 对比 veth pair
后端
Asthenia04121 小时前
Java受检异常与非受检异常分析
后端
uhakadotcom2 小时前
快速开始使用 n8n
后端·面试·github
JavaGuide2 小时前
公司来的新人用字符串存储日期,被组长怒怼了...
后端·mysql
bobz9652 小时前
qemu 网络使用基础
后端
Asthenia04122 小时前
面试攻略:如何应对 Spring 启动流程的层层追问
后端
Asthenia04123 小时前
Spring 启动流程:比喻表达
后端
Asthenia04123 小时前
Spring 启动流程分析-含时序图
后端
ONE_Gua3 小时前
chromium魔改——CDP(Chrome DevTools Protocol)检测01
前端·后端·爬虫