请收藏再看。此文篇幅太长,你短时间看不完;此文干货太多,错过太可惜。
示例代码可以关注逸飞兮
(公众号)回复jy
获取。
hibernate-validator
及类似校验工具的各种使用姿势 注意:hibernate-validator 与 持久层框架 hibernate
没有什么关系,hibernate-validator 是 hibernate 组织下的一个开源项目 。
hibernate-validator
是 JSR 380(Bean Validation 2.0)
、JSR 303(Bean Validation 1.0)
规范的实现。
JSR 380
- Bean Validation 2.0
定义了一个实体和方法验证的元数据模型和 API。
JavaEE(改名为:Jakarta EE)中制定了 validation 规范,即:javax.validation-api(现为 jakarta.validation-api,jar 包的名字改变,包里面的包名、类名未变,因此使用方式不变)包,spring-boot-starter-web
、spring-boot-starter-webflux
包都已引入此依赖,直接使用即可。
有点类似于 slf4j 与 logback(log4j2)的关系,使用的时候,代码中使用 javax.validate
提供的接口规范功能,加载的时候,根据 SPI 规范加载对应的规范实现类。
它和 hibernate
没什么关系,放心大胆的使用吧。
hibernate-validator 官方有如下说明:
使用 hibernate-validator
后,校验逻辑如下:
controller、service、dao 层相同的校验逻辑可以使用同一个数据校验模型。
标记用于验证
级联
的属性、方法参数或方法返回类型。在验证属性、方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。
此行为是
递归
应用的。
spring
提供的扩展注解,可以方便的用于分组校验
下面除了列出的参数,每个约束都有参数 message,groups 和 payload。这是 Bean Validation 规范的要求。
其中,message
是提示消息,groups
可以根据情况来分组。
以下每一个注解都可以在相同元素上定义多个。
检查元素是否为 false,支持数据类型:boolean、Boolean
检查元素是否为 true,支持数据类型:boolean、Boolean
inclusive:boolean,默认 true,表示是否包含,是否等于value:当 inclusive=false 时,检查带注解的值是否小于指定的最大值。当 inclusive=true 检查该值是否小于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最大值。支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)
支持数据类型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封装类)inclusive:boolean,默认 true,表示是否包含,是否等于value:当 inclusive=false 时,检查带注解的值是否大于指定的最大值。当 inclusive=true 检查该值是否大于或等于指定的最大值。参数值是根据 bigdecimal 字符串表示的最小值。
检查值是否为最多包含 integer
位整数和 fraction
位小数的数字支持的数据类型:BigDecimal, BigInteger, CharSequence, byte, short, int, long 、原生类型的封装类、任何 Number 子类。
检查指定的字符序列是否为有效的电子邮件地址。可选参数 regexp
和 flags
允许指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)。支持的数据类型:CharSequence
检查值是否小于或等于指定的最大值支持的数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查值是否大于或等于指定的最大值支持的数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查字符序列是否为空,以及去空格后的长度是否大于 0。与 @NotEmpty
的不同之处在于,此约束只能应用于字符序列,并且忽略尾随空格。支持数据类型:CharSequence
检查值是否不为 null
支持数据类型:任何类型
检查元素是否为 null
或 空
支持数据类型:CharSequence, Collection, Map, arrays
检查元素个数是否在 min(含)和 max(含)之间支持数据类型:CharSequence,Collection,Map, arrays
检查元素是否严格为负数。零值被认为无效。支持数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查元素是否为负或零。支持数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查元素是否严格为正。零值被视为无效。支持数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查元素是否为正或零。支持数据类型:BigDecimal, BigInteger, byte, short, int, long, 原生类型的封装类, CharSequence 的任意子类(字符序列表示的数字), Number 的任意子类, javax.money.MonetaryAmount 的任意子类
检查值是否为 null
支持数据类型:任何类型
检查日期是否在未来支持的数据类型: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 API 在类路径中,ReadablePartial
和ReadableInstant
的任何实现类
检查日期是现在或将来支持数据类型:同@Future
检查日期是否在过去支持数据类型:同@Future
检查日期是否在过去或现在支持数据类型:同@Future
根据给定的 flag
匹配,检查字符串是否与正则表达式 regex
匹配支持数据类型:CharSequence
从上文可知,规范中,@Size 支持的数据类型有:CharSequence,Collection,Map, arrayshibernate-validator 中的实现如下:
针对 CharSequence、Collection、Map 都有一个实现,由于 arrays 有多种可能,提供了多个实现。其中,SizeValidatorForCollection.java 如下:
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraints.Size;
@SuppressWarnings("rawtypes")
// as per the JLS, Collection> is a subtype of Collection, so we need to explicitly reference
// Collection here to support having properties defined as Collection (see HV-1551)
public class SizeValidatorForCollection implements ConstraintValidator {
private static final Log LOG = LoggerFactory.make( MethodHandles.lookup() );
private int min;
private int max;
@Override
public void initialize(Size parameters) {
min = parameters.min();
max = parameters.max();
validateParameters();
}
@Override
public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
if ( collection == null ) {
return true;
}
int length = collection.size();
return length >= min && length <= max;
}
private void validateParameters() {
if ( min < 0 ) {
throw LOG.getMinCannotBeNegativeException();
}
if ( max < 0 ) {
throw LOG.getMaxCannotBeNegativeException();
}
if ( max < min ) {
throw LOG.getLengthCannotBeNegativeException();
}
}
}
实现逻辑就是按照规范的说明来实现的。
可以用以下方式声明约束:
@NotNull
private String manufacturer;
@NotNull
public String getManufacturer(){
return manufacturer;
}
private Map<@NotNull FuelConsumption, @MaxAllowedFuelConsumption Integer> fuelConsumption = new HashMap<>();
@ValidPassengerCount
public class Car {
private int seatCount;
private List passengers;
//...
}
重写
,约束注解将会聚合,也就是此方法父类和子类声明的约束都会起作用。 Bean Validation API
不仅允许验证单个类实例,也支持级联验证。@Valid
修饰对象属性的引用,则对象属性中声明的所有约束也会起作用。public class Car {
@NotNull
@Valid
private Person driver;
//...
}
public class Person {
@NotNull
private String name;
//...
}
通过向方法或构造函数的参数添加约束注解来指定方法或构造函数的前置条件
,官方示例如下:
public RentalStation(@NotNull String name){}
public void rentCar(@NotNull Customer customer,
@NotNull @Future Date startDate,
@Min(1) int durationInDays){}
通过在方法体上添加约束注解来给方法或构造函数指定后置条件
,官方示例如下:
public class RentalStation {
@ValidRentalStation
public RentalStation() {
//...
}
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getCustomers() {
//...
return null;
}
}
此示例指定了三个约束:
类似于 JavaBeans 属性的级联验证,@Valid
注解可用于标记方法参数和返回值的级联验证。
类似于 javabeans 属性的级联验证(参见第 2.1.6 节“对象图”),@valid 注释可用于标记可执行参数和级联验证的返回值。当验证用@valid 注释的参数或返回值时,也会验证在参数或返回值对象上声明的约束。而且,也可用在容器元素中。
public class Garage {
public boolean checkCars(@NotNull List<@Valid Car> cars) {
//...
return false;
}
}
当在继承体系中声明方法约束时,必须了解两个规则:
这些规则是由子类行为概念所决定的:在使用类型 T 的任何地方,也能在不改变程序行为的情况下使用 T 的子类。
当两个类分别有一个同名且形参列表相同的方法,而另一个类用一个方法重写/实现上述两个类的同名方法时,这两个父类的同名方法上不能有任何参数约束,因为不管怎样都会与上述规则冲突。示例:
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) {
//...
}
}
注意
:上述的 22 个约束注解都有 groups
属性。当不指定 groups 时,默认为 Default
分组。
JSR
规范支持手动校验,不直接支持使用注解校验,不过 spring
提供了分组校验注解扩展支持,即:@Validated
,参数为 group 类集合
在某些场景下,需要定义一个组,它包含其它组的约束,可以用分组继承。如:
public class SuperCar extends Car {
@AssertTrue(
message = "Race car must have a safety belt",
groups = RaceCarChecks.class
)
private boolean safetyBelt;
// getters and setters ...
}
public interface RaceCarChecks extends Default {}
默认情况下,不管约束是属于哪个分组,它们的计算是没有特定顺序的,而在某些场景下,控制约束的计算顺序是有用的。如:先检查汽车的默认约束,再检查汽车的性能约束,最后在开车前,检查驾驶员的实际约束。可以定义一个接口,并用 @GroupSequence
来定义需要验证的分组的序列。示例:
@GroupSequence({ Default.class, CarChecks.class, DriverChecks.class })
public interface OrderedChecks {}
此分组用法与其它分组一样,只是此分组拥有按分组顺序校验的功能
定义序列的组和组成序列的组不能通过级联序列定义或组继承直接或间接地参与循环依赖关系。如果对包含此类循环的组计算,则会引发 GroupDefinitionException。
@GroupSequence
除了定义分组序列外,还允许重新定义指定类的默认分组。为此,只需将@GroupSequence
添加到类中,并在注解中用指定序列的分组替换 Default
默认分组。
@GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
public class RentalCar extends Car {}
在验证约束时,直接把其当做默认分组方式来验证
注意:此为 hibernate-validator 提供,JSR 规范不支持
可用于根据对象状态动态地重新定义默认分组序列。需要做两步:
示例:
public class RentalCarGroupSequenceProvider
implements DefaultGroupSequenceProvider {
@Override
public List> getValidationGroups(RentalCar car) {
List> defaultGroupSequence = new ArrayList>();
defaultGroupSequence.add( RentalCar.class );
if ( car != null && !car.isRented() ) {
defaultGroupSequence.add( CarChecks.class );
}
return defaultGroupSequence;
}
}
@GroupSequenceProvider(RentalCarGroupSequenceProvider.class)
public class RentalCar extends Car {
@AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
private boolean rented;
public RentalCar(String manufacturer, String licencePlate, int seatCount) {
super( manufacturer, licencePlate, seatCount );
}
public boolean isRented() {
return rented;
}
public void setRented(boolean rented) {
this.rented = rented;
}
}
如果你想把与汽车相关的检查和驾驶员检查一起验证呢?当然,您可以显式地指定验证多个组,但是如果您希望将这些验证作为默认组验证的一部分进行,该怎么办?这里@ConvertGroup 开始使用,它允许您在级联验证期间使用与最初请求的组不同的组。
在可以使用 @Valid 的任何地方,都能定义分组转换,也可以在同一个元素上定义多个分组转换
必须满足以下限制:
* @ConvertGroup 只能与 @Valid 结合使用。如果不是,则抛出 ConstraintDeclarationException。
* 在同一元素上有多个 from 值相同的转换规则是不合法的。在这种情况下,将抛出 ConstraintDeclarationException。
* from 属性不能引用分组序列。在这种情况下会抛出 ConstraintDeclarationException
*警告:*
规则不是递归执行的。将使用第一个匹配的转换规则,并忽略后续规则。例如,如果一组@ConvertGroup 声明将组 a 链接到 b,将组 b 链接到 c,则组 a 将被转换到 b,而不是 c。
示例:
// 当 driver 为 null 时,不会级联验证,使用的是默认分组,当级联验证时,使用的是 DriverChecks 分组
@Valid
@ConvertGroup(from = Default.class, to = DriverChecks.class)
private Driver driver;
三个步骤:
此处示例展示编写一个注解,确保给定字符串全是大写或全是小写。首先,定义一个枚举,列出所有情况:大写、小写
public enum CaseMode{
UPPER,
LOWER;
}
然后,定义一个约束注解
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();
}
}
Bean Validation API
规范要求任何约束注解定义以下要求:
message
属性:在违反约束的情况下返回一个默认 key 以用于创建错误消息 groups
属性:允许指定此约束所属的验证分组。必须默认是一个空 Class 数组 payload
属性:能被 Bean Validation API 客户端使用,以自定义一个注解的 payload 对象。API 本身不使用此属性。自定义 payload 可以是用来定义严重程度。如下: public class Severity{
public interface Info extends Payload{}
public interface Error extends Payload{}
}
public class ContactDetails{
@NotNull(message="名字必填", payload=Severity.Error.class)
private String name;
@NotNull(message="手机号没有指定,但不是必填项", payload=Severity.Info.class)
private String phoneNumber;
}
然后客户端在 ContactDetails 实例验证之后,可以通过 ConstraintViolation.getConstraintDescriptor().getPayload()
获取 severity ,然后根据 severity 调整其行为。此外,约束注解上还修饰了一些元注解:
创建了一个注解,还需要创建一个约束验证器,以用来验证使用注解的元素。
需要实现 Bean Validation 接口:ConstraintValidator
示例:
public class CheckCaseValidator implements ConstraintValidator {
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
指定了两个泛型类型:
需要实现两个方法:
initialize()
让你可以获取到使用注解时所指定的参数(可以将它们保存起来以供下一步使用) isValid()
包含实际的校验逻辑。注意:Bean Validation 规范建议将 null 值视为有效值。如果一个元素 null 不是一个有效值,则应该显示的用 @NotNull 标注。 isValid() 方法中的 ConstraintValidatorContext 对象参数:
当应用指定约束验证器时,提供上下文数据和操作。
此对象至少有一个 ConstraintViolation
,可以是默认的,或者自定义的。
@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 ) {
// 禁用默认 ConstraintViolation,并自定义一个
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(
"{org.hibernate.validator.referenceguide.chapter06."
"constraintvalidatorcontext.CheckCase.message}"
)
.addConstraintViolation();
}
return isValid;
}
以上官方示例展示了禁用默认消息并自定义了一个错误消息提示。hibernate-validator
提供了一个 ConstraintValidator
扩展接口,如下,此处不作详细介绍。
public interface HibernateConstraintValidator extends ConstraintValidator {
default void initialize(ConstraintDescriptor constraintDescriptor, HibernateConstraintValidatorInitializationContext initializationContext) {}
}
目前需要通过 HibernateConstraintValidator
实现,参考以下官方示例,此处不作详细介绍。
HibernateValidatorFactory hibernateValidatorFactory = Validation.byDefaultProvider()
.configure()
.buildValidatorFactory()
.unwrap( HibernateValidatorFactory.class );
Validator validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "US" )
.getValidator();
// [...] US specific validation checks
validator = hibernateValidatorFactory.usingContext()
.constraintValidatorPayload( "FR" )
.getValidator();
// [...] France specific validation checks
public class ZipCodeValidator implements ConstraintValidator {
public String countryCode;
@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if ( object == null ) {
return true;
}
boolean isValid = false;
String countryCode = constraintContext
.unwrap( HibernateConstraintValidatorContext.class )
.getConstraintValidatorPayload( String.class );
if ( "US".equals( countryCode ) ) {
// checks specific to the United States
}
else if ( "FR".equals( countryCode ) ) {
// checks specific to France
}
else {
// ...
}
return isValid;
}
}
当违反约束时,应该用到的消息需要定义一个 ValidationMessages.properties
文件,并记录以下内容:
# org.hibernate.validator.referenceguide.chapter06.CheckCase 是注解 CheckCase 的全类名
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.
如果发生验证错误,验证运行时将使用为注解的
message
属性指定的默认值来查找此资源包中的错误消息。
类级别约束,用来验证整个对象的状态。其定义方式与上述简单约束定义相同。只不过 @Target
中的值需要包含 TYPE
。
因为类级别约束验证器可以获取此类实例的所有属性,因此可以用来对其中某些属性做约束。
public class ValidPassengerCountValidator
implements ConstraintValidator {
@Override
public void initialize(ValidPassengerCount constraintAnnotation) {}
@Override
public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
if ( car == null ) {
return true;
}
// 用来验证两个属性之间必须满足一种关系
// 验证乘客数量不能大于座椅数量
boolean isValid = car.getPassengers().size() <= car.getSeatCount();
if ( !isValid ) {
constraintValidatorContext.disableDefaultConstraintViolation();
constraintValidatorContext
.buildConstraintViolationWithTemplate( "{my.custom.template}" )
.addPropertyNode( "passengers" ).addConstraintViolation();
}
return isValid;
}
}
@NotNull
@Size(min = 2, max = 14)
@CheckCase(CaseMode.UPPER)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = { })
@Documented
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06."
"constraintcomposition.ValidLicensePlate.message}";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
一个注解拥有多个注解的功能,而且此组合注解通常不需要再指定验证器。此注解验证之后会得到违反所有约束的集合,如果想违反其中一个约束之后就有对应的违约信息,可以使用 @ReportAsSingleViolation
//...
@ReportAsSingleViolation
public @interface ValidLicensePlate {
String message() default "{org.hibernate.validator.referenceguide.chapter06."
"constraintcomposition.reportassingle.ValidLicensePlate.message}";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
}
// 实体类
/** 验证参数都设置符合条件的默认值 */
@Data
public class ValidatorVO {
@NotBlank private String name = "1";
@Min(0)
@Max(200)
private Integer age = 20;
@PastOrPresent
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime birthday = LocalDateTime.now().minusDays(1);
@Digits(integer = 4, fraction = 2)
@DecimalMax(value = "1000")
@DecimalMin(value = "0")
private BigDecimal money = new BigDecimal(10);
@Email private String email = "[email protected]";
@NotNull private String username = "username";
@Size(max = 2)
private List nickname;
@Positive /*(message = "身高不能为负数")*/ private Double height = 100D;
@FutureOrPresent
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime nextBirthday = LocalDateTime.now().plusDays(1);
}
在使用此对象时,需要验证,则用 @Valid
注解修饰。
注意:需要级联验证的属性需要加上 @Valid
注解修饰,如:
// 验证参数都设置符合条件的默认值
@NotNull @Valid private HairVO hair = new HairVO();
/** 验证参数都设置符合条件的默认值 */
@Data
public class HairVO {
@Positive private Double length = 10D;
@Positive private Double Diameter = 1D;
@NotBlank private String color = "black";
}
这里的普通分组,是指单独的一个接口,没有继承
// 分组:使用一个空接口做标识
public interface HasIdGroup {}
@Data
public class ValidatorManual {
@NotNull(groups = HasIdGroup.class)
private Integer id;
}
/**
* 分组校验
* 分组不匹配时,校验注解不起作用,注意:Default 分组也不起作用
*
* 不同于 JSR-303(javax.validate) 规范的实现,提供 JSR-303 group 的扩展实现
*/
@PostMapping
public boolean addUser(@Validated(NoIdGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
/**
* 分组校验
* 分组匹配时,校验注解起作用,但这里只校验 HasIdGroup 分组,默认分组不校验
*
* 不同于 JSR-303(javax.validate) 规范的实现,提供 JSR-303 group 的扩展实现
*/
@PutMapping
public boolean updateUser(@Validated(HasIdGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
如果想要默认分组起作用,而其他分组也要校验,怎么操作呢?可以在使用的时候,指定校验多个分组,如下:
public boolean addUser1(@Validated({Default.class,NoIdGroup.class})
ValidatorVO user, BindingResult result){}
但因为此处,是想 Default
分组一直都要校验,每次都带上有些赘余,因此建议分组在定义的时候继承默认分组,如下:
public interface DefaultInherGroup extends Default {}
/** 验证参数都设置符合条件的默认值 */
@Data
public class ValidatorVO {
@NotNull (groups = HasIdGroup.class)
// 再加上继承分组
@NotNull (groups = DefaultInherGroup.class)
private Integer id = 1;
}
/**
* 接口,需要测试的对象用 @Valid 修饰
*/
@Slf4j
@RequestMapping("/user")
@RestController
public class ValidatorController {
@GetMapping
public boolean getUser(@Valid ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
}
// 测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootExampleApplicationTests {
@Autowired WebApplicationContext context;
private MockMvc mvc;
private DateTimeFormatter formatter;
@Before
public void setMvc() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(context).build();
formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
}
@Test
public void verificationFailedWhenNameIsBlank() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("name", ""))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenAgeGreaterThan200() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("age", "201"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenBirthdayIsFuture() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get("/user")
.param("birthday", formatter.format(LocalDateTime.now().plusDays(1))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenMoneyGreaterThan1000() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "1001"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenFractionOverflow() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "999.222"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenFractionOverflowAndGreaterThan1000() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("money", "1001.222"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenEmailNotMatchFormat() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("email", "111222@"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenUsernameIsNull() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("username", null))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenNicknameGreaterThan2() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("nickname", "小明", "小蓝", "小兰"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenHeightIsNotPositive() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("height", "0"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void verificationFailedWhenNextBirthdayIsPast() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get("/user")
.param("nextBirthday", formatter.format(LocalDateTime.now().minusDays(1))))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
}
/** 级联验证:当验证属性对象中包含的一个属性不满足要求,则验证失败 */
@Test
public void verificationFailedWhenPropertiesNotPassVerification() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user").param("hair.length", "-1"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
// ValidatorController.java
/**
* 分组校验
* 分组不匹配时,校验注解不起作用
*
* 不同于 JSR-303(javax.validate) 规范的实现,提供 JSR-303 group 的扩展实现
*/
@PostMapping
public boolean addUser(@Validated(NoIdGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
/**
* 分组校验
* 分组匹配时,校验注解起作用
*
* 不同于 JSR-303(javax.validate) 规范的实现,提供 JSR-303 group 的扩展实现
*/
@PutMapping
public boolean updateUser(@Validated(HasIdGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
/**
* 分组校验
* 指定多个分组进行匹配
*
* 不同于 JSR-303(javax.validate) 规范的实现,提供 JSR-303 group 的扩展实现
*/
@PostMapping("/1")
public boolean addUser1(@Validated({Default.class,NoIdGroup.class}) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
/** 注解校验,此种方式是由 spring 注解提供 */
@Test
public void validateFailedWhenGroupMatched() throws Exception {
mvc.perform(MockMvcRequestBuilders.put("/user").param("id", ""))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
@Test
public void validateSucWhenGroupNotMatched() throws Exception {
mvc.perform(MockMvcRequestBuilders.post("/user").param("id", "").param("name", ""))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
/** 匹配的分组起作用,不匹配的不起作用 */
@Test
public void validateFailedByGroup() throws Exception {
mvc.perform(MockMvcRequestBuilders.post("/user/1").param("id", "").param("name", ""))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
/** 手动使用工具校验,此种方式由 JSR 规范提供 */
@Test
public void validateSucWhenGroupNotMatched() {
ValidatorManual vm = new ValidatorManual();
Set> validateResult = validator.validate(vm);
assertEquals(0, validateResult.size());
}
@Test(expected = AssertionError.class)
public void validateFailedWhenGroupMatched() {
ValidatorManual vm = new ValidatorManual();
Set> validateResult =
validator.validate(vm, HasIdGroup.class);
for (ConstraintViolation msg : validateResult) {
log.error(msg.getMessage());
}
assertEquals(0, validateResult.size());
}
// ValidatorController.java
@GetMapping("/1")
public boolean getUser1(@Validated(DefaultInherGroup.class) ValidatorVO user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
log.error(error.getDefaultMessage());
}
return false;
}
return true;
}
// 测试类
@Test
public void validateFailedWhenGroupMatched1() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/user/1").param("id", "").param("name", ""))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("true"));
}
hibernate-validator 是根据 Java SPI 机制提供的接口,因此使用的时候只要类路径有实现类存在,代码中尽管用 javax.validate.xxxx 就可以了,如果需要切换实现类,换掉实现类就行了,使用的代码不需要改。
需要验证数据的地方很多,使用这样一个校验框架,会方便太多,代码少了,bug 少了,如果认为提示方式不够友好,可以合理扩展消息提醒、消息国际化等,也可以用 AOP 统一处理验证信息。
Bean Validation 2.0 (JSR 380)
hibernate-validator 最新版官方资料
hibernate-validator | github公众号:逸飞兮(专注于 Java 领域知识的深入学习,从源码到原理,系统有序的学习)