简介
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()
}
基本使用
以下是基本的使用:
- Validator是线程安全的,定义一个静态变量保存起来,不要每次都创建,可以多次使用
- 通过Validation.buildDefaultValidatorFactory()创建一个ValidatorFactory,然后调用getValidator()方法获取一个Validator
- 创建一个Person对象(标注了Constraint)
- 通过validator.validate(object)方法校验
- 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
- 先在Bean上声明Constraint 2. 验证bean实例是否满足Constraint Constraint即JSR规定的那些验证annotation、自定义的 constraint annotation
声明Bean Constraint
Constraint支持4中类型的标注:
- 字段
- 属性
- 容器元素
- 类
字段即平时定义的哪些入参,字段类型为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级别的,需要自己实现。
- @AssertFalse 支持Boolean, boolean
- @AssertTrue 支持Boolean, boolean
- @DecimalMax(value=, inclusive=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
- @DecimalMin(value=, inclusive=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
- @Digits(integer=, fraction=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long,高版本支持Number的子类
- @Email 支持CharSequence
- @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
- @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
- @Max(value=) 支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @Min(value=) 支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @NotBlank 支持CharSequence。验证字符串是否为空,空字符串,空格,null
- @NotEmpty 支持CharSequence, Collection, Map and arrays。验证是否为null或者长度为0
- @NotNull 支持任何类型
- @Negative 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @NegativeOrZero 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @Null 支持任何类型
- @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
- @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
- @Pattern(regex=, flags=) 支持CharSequence
- @Positive 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @PositiveOrZero 支持支持BigDecimal, BigInteger, byte, short, int, long,高版本支持纯数字的CharSequence、Number的子类
- @Size(min=, max=) 支持CharSequence, Collection, Map and arrays
- @CreditCardNumber(ignoreNonDigitCharacters=) 支持CharSequence
- @Currency(value=) 支持 javax.money.MonetaryAmount 子类
- @DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) 支持java.time.Duration
- @DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) 支持java.time.Duration
- @ISBN (type=) 支持CharSequence
- @Length(min=, max=) 支持CharSequence
- @Range(min=, max=) 支持BigDecimal, BigInteger, CharSequence, byte, short, int, long
- @UniqueElements 支持Collection子类
- @URL(protocol=, host=, port=, regexp=, flags=) 支持CharSequence
- @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 ...
}
方法验证可以被继承
当需要使用、扩展 方法验证继承的时候,需要满足一下条件:
- 前置验证的条件:父类必须比子类严格(父类设置参数验证,子类重写时不需要指定,使用父类的)
- 后置验证的条件:父类必须比子类严格(父类不设置、设置宽松,子类设置、设置严格,使用子类的)
如下为一个不合法的例子。正常情况:父类、接口设置了参数验证,子类就不允许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使用"${}"设置。 在处理消息、填充变量、表达式时有如下算法:
- 解析消息变量,将变量名作为key在classpath下的ValidationMessages.properties文件中查找。支持多语言,多语言命名样式ValidationMessages_en_US.properties。通过读取jvm的Locale#getDefault()获取语言
- 同1,只不过在 Appendix B 里进行查找。如果找到了就会再次执行1,否则执行3
- 解析消息变量,使用Constraint Annotation里的member进行填充。比如:"must be at least ${min}" 注解为:Size#min()
- 解析消息表达式,将他们作为el表达式进行计算
特殊符号
因为表达式、变量中使用的{、}、$ 有特殊含义,因此有些符号需要进行转义。
- 如果要使用{ 则需要 {
- 如果要使用} 则需要 }
- 如果要使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> 则需要 则需要 </math>则需要
- 如果要使用\ 则需要 \
填充消息表达式
在Hibernate Validate 5,支持使用Jakarta Expression Language 在message里。 这样可以在表达式里存在一些条件逻辑、格式化等。 Validate Engine规定了在EL表达式里支持一下对象:
- constraint annotation对应的参数名称。比如{min}, {max}
- 当前被验证的值,通过validatedValue这个key标识获取
- 对应的格式化器(要求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规范(需要包含三个属性):
- message 定义验证失败的提示信息
- groups 定义可以验证的分组,默认为一个空数组
- 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还绑定了一系列元注解:
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
指定了注解支持的元素类型。FIELD表示属性,METHOD表示方法返回值, PARAMETER表示方法、构造函数参数参数,ANNOTATION_TYPE表示可以标注到注解上进行组合,TYPE_USE表示可以标注到泛型实参上。当需要标注到类级别是需要设置为TYPE。 当验证的目标为构造函数的返回值=时需要设置为CONSTRUCTOR,当需要使用跨多个参数验证时需要设置METHOD or CONSTRUCTOR。@Retention(RUNTIME)
指定了在运行时可以通过反射获取到@Constraint(validatedBy = CheckCaseValidator.class):
指定在验证时使用的Constraint Validator,同时对于不同的数据类型可以设置多个不同的Constraint Validator。@Documented:
指定了在JavaDoc上进行展示@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] );
}
}