2. 声明和验证bean的约束
2.1. 声明bean约束
Bean验证中的约束通过Java注解来表示。在本节中,您将学习如何使用这些注解增强对象模型。有以下三种类型的bean约束:
- 字段约束(field constraints)
- 属性约束(property constraints)
- 类约束(class constraints)
并不是所有的约束都可以放在所有这些级别上。实际上,Bean验证所定义的默认约束都不能放在类级别上,实际上,java.lang.annotation.Target注解决定了对哪些元素进行约束。
2.1.1. 字段约束
package org.hibernate.validator.referenceguide.chapter02.fieldlevel;
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. 属性增强约束
如果模型类遵循JavaBeans标准,那么也可以对bean类的属性进行注释,而不是对其字段进行注释。属性级约束使用与字段级约束相同的实体,但是使用属性级别约束。
package org.hibernate.validator.referenceguide.chapter02.propertylevel;
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;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
@AssertTrue
public boolean isRegistered() {
return isRegistered;
}
public void setRegistered(boolean isRegistered) {
this.isRegistered = isRegistered;
}
}
属性的getter方法被注解,而不是它的setter。这种方式也可以约束只读属性,因为它没有setter方法。
当使用属性级别约束时,属性访问策略用于访问要验证的值,即验证引擎通过属性访问器方法访问状态。
建议在一个类中只使用字段或属性注解。不建议对字段和它的getter方法同时进行注解,因为这会导致字段被验证两次。
2.1.3. 类型参数约束
从Java 8开始,可以直接对类型参数进行约束。然而,这需要在约束定义中通过@target指定ElementType.TYPE_USE。为了保持向后兼容性,内置Bean验证以及Hibernate验证器特定的约束还没有指定ElementType.TYPE_USE。要使用类型参数约束,必须使用自定义约束。
类型参数约束用在参数化类型的集合、映射、java.util.Optional和自定义的参数化类型上。
对于可迭代的类型:
在对可迭代的类型参数应用约束时,Hibernate验证器将验证每个元素。
在下面的例子中,@ValidPart是一个自定义定义的类型参数约束。
package org.hibernate.validator.referenceguide.chapter02.typeargument.list;
public class Car {
@Valid
private List<@ValidPart String> parts = new ArrayList<>();
public void addPart(String part) {
parts.add( part );
}
//...
}
Car car = new Car();
car.addPart( "Wheel" );
car.addPart( null );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals(
"'null' is not a valid car part.",
constraintViolations.iterator().next().getMessage()
);
assertEquals( "parts[1].<collection element>",
constraintViolations.iterator().next().getPropertyPath().toString() );
对于映射类型:
类型参数约束将验证映射的值,而对键的约束被忽略。
package org.hibernate.validator.referenceguide.chapter02.typeargument.map;
public class Car {
public enum FuelConsumption {
CITY,
HIGHWAY
}
@Valid
private EnumMap<FuelConsumption, @MaxAllowedFuelConsumption Integer> fuelConsumption =
new EnumMap<>( FuelConsumption.class );
public void setFuelConsumption(FuelConsumption consumption, int value) {
fuelConsumption.put( consumption, value );
}
//...
}
Car car = new Car();
car.setFuelConsumption( Car.FuelConsumption.HIGHWAY, 20 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals( "20 is outside the max fuel consumption.", constraintViolations.iterator().next().getMessage() );
对于java.util.Optional:
当对可选的类型参数应用约束时,Hibernate验证器将自动展开类型并验证内部值。
package org.hibernate.validator.referenceguide.chapter02.typeargument.optional;
public class Car {
private Optional<@MinTowingCapacity(1000) Integer> towingCapacity = Optional.empty();
public void setTowingCapacity(Integer alias) {
towingCapacity = Optional.of( alias );
}
//...
}
Car car = new Car();
car.setTowingCapacity( 100 );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals( "Not enough towing capacity.",
constraintViolations.iterator().next().getMessage() );
assertEquals( "towingCapacity",
constraintViolations.iterator().next().getPropertyPath().toString() );
对于自定义参数化类型:
对自定义参数化类型进行约束时,目前有两个限制:
- 必须为自定义参数化类型注册一个ValidatedValueUnwrapper,允许检索值以验证(参见Unwrapping values章节)。
- 只支持带有一个类型参数的类型,带有两个或更多类型参数的参数化类型不检查类型参数约束。
package org.hibernate.validator.referenceguide.chapter02.typeargument.custom;
public class Car {
private GearBox<@MinTorque(100) Gear> gearBox;
public void setGearBox(GearBox<Gear> gearBox) {
this.gearBox = gearBox;
}
//...
}
package org.hibernate.validator.referenceguide.chapter02.typeargument.custom;
public class GearBox<T extends Gear> {
private final T gear;
public GearBox(T gear) {
this.gear = gear;
}
public Gear getGear() {
return this.gear;
}
}
package org.hibernate.validator.referenceguide.chapter02.typeargument.custom;
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( 100 );
}
}
}
package org.hibernate.validator.referenceguide.chapter02.typeargument.custom;
public class GearBoxUnwrapper extends ValidatedValueUnwrapper<GearBox> {
@Override
public Object handleValidatedValue(GearBox gearBox) {
return gearBox == null ? null : gearBox.getGear();
}
@Override
public Type getValidatedValueType(Type valueType) {
return Gear.class;
}
}
Car car = new Car();
car.setGearBox( new GearBox<>( new Gear.AcmeGear() ) );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals( "Gear is not providing enough torque.", constraintViolations.iterator().next().getMessage() );
assertEquals( "gearBox", constraintViolations.iterator().next().getPropertyPath().toString() );
2.1.4. 类约束
约束也可以放在类级别上,在这种情况下,被验证的不是一个属性,而是一个完整的对象。如果验证依赖于对象的多个属性之间的相关性,那么类级约束是有用的。
例如:
汽车类有两个属性,座位和乘客,应该确保乘客数量比座位数少。在类级别上添加了@ValidPassengerCount约束,该约束的验证器可以访问完整的汽车对象,允许比较座椅和乘客的数量。
package org.hibernate.validator.referenceguide.chapter02.classlevel;
@ValidPassengerCount
public class Car {
private int seatCount;
private List<Person> passengers;
//...
}
2.1.5. 约束继承
当一个类实现一个接口或扩展另一个类时,在父类上声明的所有约束注解都以指定的约束相同的方式应用与子类自身。为了让事情更清楚些,我们来看看下面的例子:
package org.hibernate.validator.referenceguide.chapter02.inheritance;
public class Car {
private String manufacturer;
@NotNull
public String getManufacturer() {
return manufacturer;
}
//...
}
package org.hibernate.validator.referenceguide.chapter02.inheritance;
public class RentalCar extends Car {
private String rentalStation;
@NotNull
public String getRentalStation() {
return rentalStation;
}
//...
}
在这里,出租汽车是汽车的一个子类,并添加了rentalStation属性。如果验证RentalCar的实例,不仅会对rentalStation上的@ notnull约束进行验证,还会对来自父类的manufacturer的约束进行验证。
如果Car不是超类,而是由RentalCar实现的接口,那也是一样的。
如果方法被覆盖,约束注解将被合并。因此,如果RentalCar在Car上覆盖了getManufacturer()方法,那么除了@notnull约束被验证,在重写的方法中注解的其他约束也将进行验证。
public class Car {
@NotNull//不能为null
private String manufacturer;
@NotNull//不能为null
@Size(min = 2, max = 14)//在2~14个字符之间
private String licensePlate;
@Min(2)//最小为2
private int seatCount;
public Car(String manufacturer, String licencePlate, int seatCount) {
this.manufacturer = manufacturer;
this.licensePlate = licencePlate;
this.seatCount = seatCount;
}
public String getManufacturer() {
return manufacturer;
}
public void setManufacturer(String manufacturer) {
this.manufacturer = manufacturer;
}
public String getLicensePlate() {
return licensePlate;
}
public void setLicensePlate(String licensePlate) {
this.licensePlate = licensePlate;
}
public int getSeatCount() {
return seatCount;
}
public void setSeatCount(int seatCount) {
this.seatCount = seatCount;
}
}
package test;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class RentalCar extends Car {
private String rentalStation;
@NotNull
public String getRentalStation() {
return rentalStation;
}
public void setRentalStation(String rentalStation) {
this.rentalStation = rentalStation;
}
@Override
@Size(min = 2, max = 5)//在2~14个字符之间
public String getManufacturer() {
return super.getManufacturer();
}
public RentalCar(String manufacturer, String licencePlate, int seatCount, String rentalStation) {
super(manufacturer, licencePlate, seatCount);
this.rentalStation = rentalStation;
}
}
@Test
public void test1() {
try {
RentalCar car = new RentalCar( "AA-BB-123456", "DD-AB-123", 2, "bbb" );
ValidatorUtils.validateEntity(car);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
控制台:
如果给manufacturer属性赋null值,则控制台打印不能为空,@NotNull跟@Size(min = 2, max = 5)都会起作用。
2.1.6. 级联验证
Bean验证API不仅允许验证单个类实例,而且还允许完成级联验证。为此,只需对一个字段或属性使用@Valid注解,表示引用另一个对象的引用并使用级联验证。
package org.hibernate.validator.referenceguide.chapter02.objectgraph;
public class Car {
@NotNull
@Valid
private Person driver;
//...
package org.hibernate.validator.referenceguide.chapter02.objectgraph;
public class Person {
@NotNull
private String name;
//...
}
如果验证了汽车的实例,则引用的Person对象也将被验证,因为driver字段是用@valid标注的。因此,如果引用的Person实例的name字段为空,那么验证汽车将失败。
级联验证的验证是递归的,也就是说,如果一个对象被标记为级联验证,它本身也有属性被标记为级联验证,那么这些引用也将被验证引擎跟踪。验证引擎将确保在级联验证过程中不会发生无限循环,例如,如果两个对象相互引用。
注意,在级联验证期间,null值被忽略。
级联验证也适用于集合类型字段。这意味着任何属性的:
- 数组
- 实现了java.lang.Iterable(Collection,List和Set)
- 实现了java.util.Map
都可以用@valid进行标记,当父对象被验证时,它将导致每个包含的元素被验证。
2.2. 验证bean约束
Validator接口是Bean Validation中最重要的对象。
2.2.1. 获得一个验证器实例
通过Validation类中的buildDefaultValidatorFactory()方法获取一个ValidatorFactory。然后通过ValidatorFactory获得一个Validator实例。
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
2.2.2. 验证器的方法
Validator接口包含三种方法,可以用来验证整个实体或实体的单个属性。
所有这三种方法都返回一个Set<ConstraintViolation>
。如果验证成功,则Set为空。否则,每个违反的约束将产生一个ConstraintViolation对象,并添加至Set<ConstraintViolation>
中。
所有的验证方法都有一个var- args参数,可以用来指定,在执行验证时应该考虑哪个验证组。如果没有指定,则使用默认的参数验证组(javax.validation.groups.Default)。
- validate():
使用validate()方法对给定bean的所有约束执行验证。使用Validator.validate()对汽车类实例进行验证,Car的属性值无法满足制造商属性的@notnull约束。因此,返回一个ConstraintViolation对象。
Car car = new Car( null, true );
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
assertEquals( 1, constraintViolations.size() );
assertEquals( "may not be null", constraintViolations.iterator().next().getMessage() );
- validateProperty():
使用validateProperty(),可以验证给定对象的一个指定属性。属性名是javabean属性名。
Car car = new Car( null, true );
Set<ConstraintViolation<Car>> constraintViolations = validator.validateProperty( car, "manufacturer" );
assertEquals( 1, constraintViolations.size() );
assertEquals( "may not be null", constraintViolations.iterator().next().getMessage() );
- validateValue():
通过使用validateValue()方法,可以检查特定的值是否符合给定类的单个属性的约束。
Set<ConstraintViolation<Car>> constraintViolations = validator.validateValue(
Car.class,
"manufacturer",
null
);
assertEquals( 1, constraintViolations.size() );
assertEquals( "may not be null", constraintViolations.iterator().next().getMessage() );
2.2.3. ConstraintViolation的方法
getMessage():返回错误信息。例如:不能为null。
getMessageTemplate():返回错误信息相对于ValidatorMessages.properties文件中的键值。例如:{javax.validation.constraints.NotNull.message},会根据地方的信息来查找不同的ValidatorMessages.properties文件,比如中国是ValidationMessages_zh_CN.properties,文件其中的内容是javax.validation.constraints.NotNull.message = \u4E0D\u80FD\u4E3Anull,所以getMessage()方法返回的信息是中文的。
getRootBean():获取被校验的根实体对象。
getRootBeanClass():获取被校验的根实体类。
getLeafBean():如果约束是添加在一个Bean上的,那么则返回这个bean的实例,如果约束是定义一个属性上的,则返回这个属性所属的bean的实例对象。
getPropertyPath():返回被验证了且违反了约束的属性的Path类型。
getInvalidValue():返回未能通过验证的值。
getConstraintDescriptor():返回一个ConstraintDescriptor,该类描述一个约束及其构成。
2.3. 内置的约束
2.3.1. Bean约束
约束 | 支持的类型 | 作用 |
---|---|---|
@AssertFalse | Boolean, boolean | 验证元素是否为false |
@AssertTrue | Boolean, boolean | 验证元素是否为true |
@DecimalMax(value=, inclusive=) | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及原始类型的相关包装类型 | 当inclusive为false时,被标注的值必须小于指定的值;当inclusive为true时,被标注的值必须小于等于指定的值,这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示,小数存在精度。 |
@DecimalMin(value=, inclusive=) | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及原始类型的相关包装类型 | 当inclusive为false时,被标注的值必须大于指定的值;当inclusive为true时,被标注的值必须大于等于指定的值,这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示,小数存在精度。 |
@Digits(integer=, fraction=) | BigDecimal、BigInteger、CharSequence、byte、short、int、long以及原始类型的相关包装类型 | 被标注的值必须在integer位整数和fraction位小数的范围内。 |
@Future | java.util.Date, java.util.Calendar, java.time.chrono.ChronoZonedDateTime, java.time.Instant, java.time.OffsetDateTime | 检验给定的日期是否比现在晚 |
@Max(value=) | BigDecimal、BigInteger、byte、short、int、long以及原始类型的相关包装类型 | 检查带注释的值是否小于或等于指定的值 |
@Min(value=) | BigDecimal、BigInteger、byte、short、int、long以及原始类型的相关包装类型 | 检查带注释的值是否大于或等于指定的值 |
@NotNull | Any type | 检验被标注的值不为空 |
@Null | Any type | 检验被标注的值为空 |
@Past | java.util.Date, java.util.Calendar, java.time.chrono.ChronoZonedDateTime, java.time.Instant, java.time.OffsetDateTime | 检验给定的日期是否比现在早 |
@Pattern(regex=, flags=) | CharSequence | 检查被注解的值是否与给定匹配标志的正则表达式相匹配 |
@Size(min=, max=) | CharSequence, Collection, Map and arrays | 检查被注解元素的大小是否介于min和max之间(包含) |
@Valid | 任何非基本类型 | 递归地对相关对象执行验证。如果对象是一个集合或数组,那么元素将被递归验证。如果对象是映射,则递归地验证值元素。 |
2.3.2. 附加的约束
除了 Bean Validation API定义的约束,Hibernate Validator提供几个有用的自定义约束,这些约束适用于字段/属性级别,只有@scriptassert是类级约束。
约束 | 支持的类型 | 作用 |
---|---|---|
@CreditCardNumber(ignoreNonDigitCharacters=) | CharSequence | 字符串必须是信用卡号(按美国的标准验的),此验证旨在检查用户的错误,而不是信用卡的有效性!ignoreNonDigitCharacters允许忽略非数字字符,默认的是false。 |
@Currency(value=) | 任何javax.money.MonetaryAmount的子类 | 检查被注解的MonetaryAmount的货币单位是不是在value指定的货币单位里面 |
@EAN(type=) | CharSequence | 检查被注解的字符序列是否是一个有效的EAN条形码。类型决定了条形码的类型。默认值是EAN-13。 |
@Email(regexp=,flags=) | CharSequence | 检查指定的字符序列是否是一个有效的电子邮件地址。可选参数regexp和flags允许指定一个额外的给定匹配标志的正则表达式 |
@Length(min=, max=) | CharSequence | 验证带注释的字符序列位于最小值和最大值之间 |
@NotBlank | CharSequence | 校验字符不为null并且trim之后的长度是大于0的 |
@NotEmpty | CharSequence, Collection, Map and arrays | 被标注的的元素不能为null也不能为空(比如空字串,空数组) |
@Range(min=, max=) | BigDecimal, BigInteger, CharSequence, byte, short, int, long以及原始类型的相关包装类型 | 检查被标识的值是否位于指定的最小值和最大值之间 |
@SafeHtml(whitelistType= , additionalTags=, additionalTagsWithAttributes=) | CharSequence | 检查被标识的值是否包含潜在的恶意片段,例如< script/ >。如果使用该约束,需要把jsoup包放到类路径下。通过whitelistType中的预定义的白名单设置哪些标签可以通过,可以通过additionalTags,additionalTagsWithAttributes实现更精确的控制,前者允许添加没有任何属性的标记,而后者允许使用注释@safehtml.tag指定标记和可选的属性。例如:@SafeHtml(whitelistType = WhiteListType.RELAXED, additionalTagsWithAttributes = { @SafeHtml.Tag(name = "p", attributes = { "style" }) }) |
@URL(protocol=, host=, port=, regexp=, flags=) | CharSequence | 根据RFC2396,检查被标注的字符序列是否是有效的URL。如果指定了任意参数协议、主机或端口,那么相应的URL片段必须匹配指定的值。可选参数regexp和flags允许指定一个额外的给定匹配标志的正则表达式。在默认情况下,此约束使用java .net. URL构造函数来验证给定的字符串是否表示有效的URL。基于正则表达式的版本也可以使用RegexpURLValidator类,它可以通过XML或者编程API进行配置。 |
附加的约束还有:
- @LuhnCheck(startIndex= , endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)
- @Mod10Check(multiplier=, weight=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)
- @Mod11Check(threshold=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=, treatCheck10As=, treatCheck11As=)
- @ScriptAssert(lang=, script=, alias=, reportOn=)
还有一些国家特定的约束就不罗列出来了。