一、前言
验证数据是贯穿所有应用程序层(从表示层到持久性层)的常见任务。通常在每一层都要实现相同的验证逻辑,这既耗时又容易出错。而直接将这些验证逻辑捆绑到域模型中,也会使域模型变得非常混乱,因为这会让域模型类充斥验证代码。
因为验证代码实际上是类本身的元数据,所以不妨直接在域模型上添加特定注解,直接约束域模型的值。实际上,在JSR 380 - Bean Validation 2.0
中就约定了一套用于验证实体和方法的元数据模型(默认为注解,也可以使用xml)和API,极大的简化了参数验证的复杂度。
Hibernate Validator
是JSR 380
的一个实现。实现本身以及bean验证API和tck都是在Apache软件许可证2.0下提供和分发的。
Hibernate Validator 6
和Bean Validation 2.0
要求Java 8或更高版本。
基本用法
由于Spring或其他持久层框架一般都集成了HibernateValidator,所以这里不再介绍如何配置开发环境。
下面我们以一个汽车类为例,来讲一下如何使用JSR308注解
第一步,添加约束
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class Car {
@NotNull(message="汽车制造厂商必须不为空")
private String manufacturer;//汽车的制造厂商
@NotNull(message="汽车牌照不允许为空")
@Size(min = 2, max = 14, message="车牌号必须在2~14个字符")
private String licensePlate;//汽车牌照
@Min(value=2, message="汽车必须至少拥有2个座位")
private int seatCount;//汽车座位数
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
//getters and setters ...
}
@NotNull
, @Size
和 @Min
这几个注解用于声明用于汽车实例字段的约束:
- manufacturer必须非空
- licensePlate必须不为空,且长度在2到14个字符
- seatCount必须大于等于2
第二步,验证约束
下面将写几个测试用例来测试一下我们添加的约束是否有用:
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CarTest {
private static Validator validator;
@BeforeClass
public static void setUpValidator() {//初始化validator
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
public void manufacturerIsNull() {//测试生产商为空的情况
Car car = new Car( null/*生产商为空*/, "苏A-0123", 4 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);
assertEquals(1, constraintViolations.size());
assertEquals("汽车制造厂商必须不为空", constraintViolations.iterator().next().getMessage());
}
@Test
public void licensePlateTooShort() {//测试车牌号长度过小的情况
Car car = new Car( "Morris", "D" /*车牌号长度为1*/, 4 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals(1, constraintViolations.size());
assertEquals("车牌号必须在2~14个字符",constraintViolations.iterator().next().getMessage());
}
@Test
public void seatCountTooLow() {//测试座位数小于2的情况
Car car = new Car( "Morris", "苏A-12345", 1 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(car);
assertEquals(1, constraintViolations.size());
assertEquals("汽车必须至少拥有2个座位", constraintViolations.iterator().next().getMessage());
}
@Test
public void carIsValid() {//测试满足所有约束的情况
Car car = new Car( "Morris", "苏A-12345", 2 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 0, constraintViolations.size() );
}
}
我们可以发现,只要在实体类中添加几个注解,然后直接用validator验证就可以很方便地进行验证操作。
那么接下来将详细地介绍如何使用这些约束,以及其他更详尽的用法
二、声明并使用Bean约束
2.1.如何添加约束
2.1.1.添加Field-level constraints
public class Car {
@NotNull
private String manufacturer;
@AssertTrue
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
//getters and setters...
}
2.1.2.添加Property-level constraints
如果模型类采用了JavaBeans规范,也可以不在字段上,而是在字段的properties上添加注解。
public class Car {
private String manufacturer;
private boolean isRegistered;
public Car(String manufacturer, boolean isRegistered) {
this.manufacturer = manufacturer;
this.isRegistered = isRegistered;
}
@NotNull
public String getManufacturer() {
return manufacturer;
}
@AssertTrue
public boolean isRegistered() {
return isRegistered;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public void setRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
}
这种写法会让validator通过使用getter来获取字段值并验证。
不推荐在字段和字段的getter上都添加注解,因为这会让字段被验证两次
2.1.3.添加Container element constraints
当我们需要对一个集合内的元素进行验证时,就不能直接在字段上添加注解了,这个时候需要在泛型参数上添加注解:
import java.util.HashSet;
import java.util.Set;
public class Car {
private Set<@ValidPart String> parts = new HashSet<>();
public void addPart(String part) {
parts.add( part );
}
//...
}
hibernate validator支持如下集合:Iterable
, List
, Map
(key和value都可以), java.util.Optional
, 自定义容器类
, 容器类嵌套
容器类嵌套:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotNull;
public class Car {
private Map<@NotNull Part, List<@NotNull Manufacturer>> partManufacturers =
new HashMap<>();
//...
}
2.1.4.添加Class-level constraints
约束同样可以被添加到一个类上。这时进行参数验证的主体将是整个类。这在一个类中有多个参数需要进行协同验证时非常有用
@ValidPassengerCount
public class Car {
private int seatCount;
private List<Person> passengers;
//...
}
2.1.5.约束继承Constraint inheritance
约束会被继承。如果子类继承了父类,那么子类同样会继承父类的约束,下面来看个例子:
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;
}
//...
}
在这里,RentalCar 是汽车的一个子类,并添加了属性rentalStation。如果验证了RentalCar 的实例,那么不仅要计算rentalStation上的@NotNull约束,还要计算父类对制造商的约束。
如果Car 不是一个超类,而是一个由RentalCar 实现的接口,那么情况也是一样的。
如果方法被重写,约束注释将被聚合。因此,如果RentalCar覆写了Car中的getmanufacturer()方法,那么除了超类中的@NotNull约束之外,还将对覆盖方法中注释的任何约束进行评估。
2.1.6.Object graphs
Bean Validation API不仅允许验证单个类实例,还允许完成级联验证。要这么做,只要在一个字段或属性上添加@Valid注解即可
public class Car {
@NotNull
@Valid
private Person driver;
//...
}
public class Person {
@NotNull
private String name;
//...
}
如上面的代码,如果一个Car的实例被验证了,那么其包含的Person对象也会被验证(driver字段添加了@Valid注解)所以如果Person中的name字段为空,就会导致这整个验证产生错误。
值得一提的是,这种验证是递归的,也就是说所有添加了@Validate注解的 子属性、子属性的子属性等等都会被验证。不过验证引擎会保证不会出现死循环,比如两个对象互相持有对方的引用的情况。
另外,在级联验证中会忽略空值。
级联验证同样适用于容器类型。
2.2.如何验证约束
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
executableValidator = factory.getValidator().forExecutables();
2.3.内置约束
注解 | 描述 | 支持的类型 | Hibernate metadata impact |
---|---|---|---|
@AssertFalse | Checks that the annotated element is false | Boolean, boolean | None |
@AssertTrue | Checks that the annotated element is true | Boolean, boolean | None |
@DecimalMax(value=, inclusive=) | Checks whether the annotated value is less than the specified maximum, when inclusive=false. Otherwise whether the value is less than or equal to the specified maximum. The parameter value is the string representation of the max value according to the BigDecimal string representation. | BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of Number and javax.money.MonetaryAmount (if the JSR 354 API and an implementation is on the class path) | None |
@DecimalMin(value=, inclusive=) | Checks whether the annotated value is larger than the specified minimum, when inclusive=false. Otherwise whether the value is larger than or equal to the specified minimum. The parameter value is the string representation of the min value according to the BigDecimal string representation. | BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of Number and javax.money.MonetaryAmount | None |
@Digits(integer=, fraction=) | Checks whether the annotated value is a number having up to integer digits and fraction fractional digits | BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of Number | Defines column precision and scale |
Checks whether the specified character sequence is a valid email address. The optional parameters regexp and flags allow to specify an additional regular expression (including regular expression flags) which the email must match. | CharSequence | None | |
@Future | Checks whether the annotated date is in the 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; additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | None |
@FutureOrPresent | Checks whether the annotated date is in the present or in the 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; additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | None |
@Max(value=) | Checks whether the annotated value is less than or equal to the specified maximum | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | Adds a check constraint on the column |
@Min(value=) | Checks whether the annotated value is higher than or equal to the specified minimum | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | Adds a check constraint on the column |
@NotBlank | Checks that the annotated character sequence is not null and the trimmed length is greater than 0. The difference to | @NotEmpty is that this constraint can only be applied on character sequences and that trailing white-spaces are ignored. | |
@NotEmpty | Checks whether the annotated element is not null nor empty | CharSequence, Collection, Map and arrays | None |
@NotNull | Checks that the annotated value is not null | Any type | Column(s) are not nullable |
@Negative | Checks if the element is strictly negative. Zero values are considered invalid. | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | None |
@NegativeOrZero | Checks if the element is negative or zero. | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | None |
@Null | Checks that the annotated value is null | Any type | None |
@Past | Checks whether the annotated date is in the 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; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | None |
@PastOrPresent | Checks whether the annotated date is in the past or in the present | 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; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | None |
@Pattern(regex=, flags=) | Checks if the annotated string matches the regular expression regex considering the given flag match | CharSequence | None |
@Positive | Checks if the element is strictly positive. Zero values are considered invalid. | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | None |
@PositiveOrZero | Checks if the element is positive or zero. | BigDecimal, BigInteger, byte, short, int, long and the respective wrappers of the primitive types; additionally supported by HV: any sub-type of CharSequence (the numeric value represented by the character sequence is evaluated), any sub-type of Number and javax.money.MonetaryAmount | None |
@Size(min=, max=) | Checks if the annotated element’s size is between min and max (inclusive) | CharSequence, Collection, Map and arrays | Column length will be set to max |
2.4.附加约束
In addition to the constraints defined by the Bean Validation API, Hibernate Validator provides several useful custom constraints which are listed below. With one exception also these constraints apply to the field/property level, only @ScriptAssert is a class-level constraint.
注解 | 描述 | 支持的类型 | Hibernate metadata impact |
---|---|---|---|
@CreditCardNumber(ignoreNonDigitCharacters=) | Checks that the annotated character sequence passes the Luhn checksum test. Note, this validation aims to check for user mistakes, not credit card validity! See also Anatomy of a credit card number. ignoreNonDigitCharacters allows to ignore non digit characters. The default is false | CharSequence | None |
@Currency(value=) | Checks that the currency unit of the annotated javax.money.MonetaryAmount is part of the specified currency units | any sub-type of javax.money.MonetaryAmount (if the JSR 354 API and an implementation is on the class path) | None |
@DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) | Checks that annotated java.time.Duration element is not greater than the one constructed from annotation parameters. Equality is allowed if inclusive flag is set to true | java.time.Duration | None |
@DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=) | Checks that annotated java.time.Duration element is not less than the one constructed from annotation parameters. Equality is allowed if inclusive flag is set to true | java.time.Duration | None |
@EAN | Checks that the annotated character sequence is a valid EAN barcode. type determines the type of barcode. The default is EAN-13 | CharSequence | None |
@ISBN | Checks that the annotated character sequence is a valid ISBN. type determines the type of ISBN. The default is ISBN-13 | CharSequence | None |
@Length(min=, max=) | Validates that the annotated character sequence is between min and max include | CharSequence | Column length will be set to max |
@CodePointLength(min=, max=, normalizationStrategy=) | Validates that code point length of the annotated character sequence is between min and max included. Validates normalized value if normalizationStrategy is set | CharSequence | None |
@LuhnCheck(startIndex= , endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=) | Checks that the digits within the annotated character sequence pass the Luhn checksum algorithm (see also Luhn algorithm). startIndex and endIndex allow to only run the algorithm on the specified sub-string. checkDigitIndex allows to use an arbitrary digit within the character sequence as the check digit. If not specified it is assumed that the check digit is part of the specified range. Last but not least, ignoreNonDigitCharacters allows to ignore non digit characters | CharSequence | None |
@Mod10Check(multiplier=, weight=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=) | Checks that the digits within the annotated character sequence pass the generic mod 10 checksum algorithm. multiplier determines the multiplier for odd numbers (defaults to 3), weight the weight for even numbers (defaults to 1). startIndex and endIndex allow to only run the algorithm on the specified sub-string. checkDigitIndex allows to use an arbitrary digit within the character sequence as the check digit. If not specified it is assumed that the check digit is part of the specified range. Last but not least, ignoreNonDigitCharacters allows to ignore non digit characters | CharSequence | None |
@Mod11Check(threshold=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=, treatCheck10As=, treatCheck11As=) | Checks that the digits within the annotated character sequence pass the mod 11 checksum algorithm. threshold specifies the threshold for the mod11 multiplier growth; if no value is specified the multiplier will grow indefinitely. treatCheck10As and treatCheck11As specify the check digits to be used when the mod 11 checksum equals 10 or 11, respectively. Default to X and 0, respectively. startIndex, endIndex checkDigitIndex and ignoreNonDigitCharacters carry the same semantics as in | @Mod10Check. | CharSequence |
@Range(min=, max=) | Checks whether the annotated value lies between (inclusive) the specified minimum and maximu | BigDecimal, BigInteger, CharSequence, byte, short, int, long and the respective wrappers of the primitive types | None |
@SafeHtml(whitelistType= , additionalTags=, additionalTagsWithAttributes=, baseURI=) | Checks whether the annotated value contains potentially malicious fragments such as script. In order to use this constraint, the jsoup library must be part of the class path. With the whitelistType attribute a predefined whitelist type can be chosen which can be refined via additionalTags or additionalTagsWithAttributes. The former allows to add tags without any attributes, whereas the latter allows to specify tags and optionally allowed attributes as well as accepted protocols for the attributes using the annotation | @SafeHtml.Tag. In addition, baseURI allows to specify the base URI used to resolve relative URIs. | CharSequence |
@ScriptAssert(lang=, script=, alias=, reportOn=) | Checks whether the given script can successfully be evaluated against the annotated element. In order to use this constraint, an implementation of the Java Scripting API as defined by JSR 223 (“Scripting for the JavaTM Platform”) must be a part of the class path. The expressions to be evaluated can be written in any scripting or expression language, for which a JSR 223 compatible engine can be found in the class path. Even though this is a class-level constraint, one can use the reportOn attribute to report a constraint violation on a specific property rather than the whole object | Any type | None |
@UniqueElements | Checks that the annotated collection only contains unique elements. The equality is determined using the equals() method. The default message does not include the list of duplicate elements but you can include it by overriding the message and using the {duplicates} message parameter. The list of duplicate elements is also included in the dynamic payload of the constraint violation | Collection | None |
@URL(protocol=, host=, port=, regexp=, flags=) | Checks if the annotated character sequence is a valid URL according to RFC2396. If any of the optional parameters protocol, host or port are specified, the corresponding URL fragments must match the specified values. The optional parameters regexp and flags allow to specify an additional regular expression (including regular expression flags) which the URL must match. Per default this constraint used the java.net.URL constructor to verify whether a given string represents a valid URL. A regular expression based version is also available - RegexpURLValidator - which can be configured via XML (see Section 8.2, “Mapping constraints via constraint-mappings”) or the programmatic API (see Section 12.13.2, “Adding constraint definitions programmatically”) | CharSequence | None |
四、添加异常提示
五、约束分组
六、自定义约束
1. Create a constraint annotation
package org.hibernate.validator.referenceguide.chapter06;
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();
}
}
2. Implement a validator
package org.hibernate.validator.referenceguide.chapter06;
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() );
}
}
}