請收藏再看。此文篇幅太長,你短期看不完;此文乾貨太多,錯過太惋惜。html
示例代碼能夠關注逸飛兮
(公衆號)回覆jy
獲取。java
hibernate-validator
及相似校驗工具的各類使用姿式注意:hibernate-validator 與 持久層框架 hibernate
沒有什麼關係,hibernate-validator 是 hibernate 組織下的一個開源項目 。git
hibernate-validator
是 JSR 380(Bean Validation 2.0)
、JSR 303(Bean Validation 1.0)
規範的實現。github
JSR 380
- Bean Validation 2.0
定義了一個實體和方法驗證的元數據模型和 API。web
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 規範加載對應的規範實現類。spring
它和 hibernate
沒什麼關係,放心大膽的使用吧。segmentfault
hibernate-validator 官方有以下說明:api
之前的校驗以下:數組
使用 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, arrays
hibernate-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<Size, Collection> { 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<Person> passengers; //... }
當一個類繼承/實現另外一個類時,父類聲明的全部約束也會應用在子類繼承的對應屬性上。
若是方法重寫
,約束註解將會聚合,也就是此方法父類和子類聲明的約束都會起做用。
Bean Validation API
不只容許驗證單個類實例,也支持級聯驗證。
只需使用 @Valid
修飾對象屬性的引用,則對象屬性中聲明的全部約束也會起做用。
如如下示例,當驗證 Car 實例時,Person 對象中的 name 字段也會驗證。
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<RentalCar> { @Override public List<Class<?>> getValidationGroups(RentalCar car) { List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>(); 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<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
指定了兩個泛型類型:
須要實現兩個方法:
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<A extends Annotation, T> extends ConstraintValidator<A, T> { default void initialize(ConstraintDescriptor<A> 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<ZipCode, String> { 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<ValidPassengerCount, Car> { @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 = "123456@qq.com"; @NotNull private String username = "username"; @Size(max = 2) private List<String> 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 分組也不起做用 * <p> * 不一樣於 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 分組,默認分組不校驗 * <p> * 不一樣於 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 /** * 分組校驗 * 分組不匹配時,校驗註解不起做用 * <p> * 不一樣於 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; } /** * 分組校驗 * 分組匹配時,校驗註解起做用 * <p> * 不一樣於 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; } /** * 分組校驗 * 指定多個分組進行匹配 * <p> * 不一樣於 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<ConstraintViolation<ValidatorManual>> validateResult = validator.validate(vm); assertEquals(0, validateResult.size()); } @Test(expected = AssertionError.class) public void validateFailedWhenGroupMatched() { ValidatorManual vm = new ValidatorManual(); Set<ConstraintViolation<ValidatorManual>> 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 統一處理驗證信息。
hibernate-validator | github
公衆號:逸飛兮(專一於 Java 領域知識的深刻學習,從源碼到原理,系統有序的學習)