validator 自動化校驗

来源:https://www.cnblogs.com/lw5946/archive/2019/09/23/11574987.html
-Advertisement-
Play Games

溫馨提示 請收藏再看。此文篇幅太長,你短時間看不完;此文乾貨太多,錯過太可惜。 示例代碼可以關註 (公眾號)回覆 獲取。 收穫 1. 講解詳細:能讓你掌握使用 及類似校驗工具的各種使用姿勢 2. 內容全面:可以當做知識字典來查詢 what 註意:hibernate validator 與 持久層框架 ...


check

溫馨提示

請收藏再看。此文篇幅太長,你短時間看不完;此文乾貨太多,錯過太可惜。

示例代碼可以關註逸飛兮(公眾號)回覆jy獲取。

收穫

  1. 講解詳細:能讓你掌握使用 hibernate-validator 及類似校驗工具的各種使用姿勢
  2. 內容全面:可以當做知識字典來查詢

what

註意:hibernate-validator 與 持久層框架 hibernate 沒有什麼關係,hibernate-validator 是 hibernate 組織下的一個開源項目

hibernate-validatorJSR 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-webspring-boot-starter-webflux 包都已引入此依賴,直接使用即可。

有點類似於 slf4j 與 logback(log4j2)的關係,使用的時候,代碼中使用 javax.validate 提供的介面規範功能,載入的時候,根據 SPI 規範載入對應的規範實現類。

它和 hibernate 沒什麼關係,放心大膽的使用吧。

why

hibernate-validator 官方有如下說明:

以前的校驗如下:
file

使用 hibernate-validator 後,校驗邏輯如下:
file

controller、service、dao 層相同的校驗邏輯可以使用同一個數據校驗模型。

how

標識註解

@Valid(規範、常用)

標記用於驗證級聯的屬性、方法參數或方法返回類型。
在驗證屬性、方法參數或方法返回類型時,將驗證在對象及其屬性上定義的約束。
此行為是遞歸應用的。

@Validated(spring)

spring 提供的擴展註解,可以方便的用於分組校驗

22 個約束註解

下麵除了列出的參數,每個約束都有參數 message,groups 和 payload。這是 Bean Validation 規範的要求。

其中,message 是提示消息,groups 可以根據情況來分組。

以下每一個註解都可以在相同元素上定義多個。

@AssertFalse

檢查元素是否為 false,支持數據類型:boolean、Boolean

@AssertTrue

檢查元素是否為 true,支持數據類型:boolean、Boolean

@DecimalMax(value=, inclusive=)

inclusive:boolean,預設 true,表示是否包含,是否等於
value:當 inclusive=false 時,檢查帶註解的值是否小於指定的最大值。當 inclusive=true 檢查該值是否小於或等於指定的最大值。參數值是根據 bigdecimal 字元串表示的最大值。
支持數據類型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封裝類)

@DecimalMin(value=, inclusive=)

支持數據類型:BigDecimal、BigInteger、CharSequence、(byte、short、int、long 和其封裝類)
inclusive:boolean,預設 true,表示是否包含,是否等於
value:
當 inclusive=false 時,檢查帶註解的值是否大於指定的最大值。當 inclusive=true 檢查該值是否大於或等於指定的最大值。參數值是根據 bigdecimal 字元串表示的最小值。

@Digits(integer=, fraction=)

檢查值是否為最多包含 integer 位整數和 fraction 位小數的數字
支持的數據類型:
BigDecimal, BigInteger, CharSequence, byte, short, int, long 、原生類型的封裝類、任何 Number 子類。

@Email

檢查指定的字元序列是否為有效的電子郵件地址。可選參數 regexpflags 允許指定電子郵件必須匹配的附加正則表達式(包括正則表達式標誌)。
支持的數據類型:CharSequence

@Max(value=)

檢查值是否小於或等於指定的最大值
支持的數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Min(value=)

檢查值是否大於或等於指定的最大值
支持的數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@NotBlank

檢查字元序列是否為空,以及去空格後的長度是否大於 0。與 @NotEmpty 的不同之處在於,此約束只能應用於字元序列,並且忽略尾隨空格。
支持數據類型:CharSequence

@NotNull

檢查值是否null
支持數據類型:任何類型

@NotEmpty

檢查元素是否為 null
支持數據類型:CharSequence, Collection, Map, arrays

@Size(min=, max=)

檢查元素個數是否在 min(含)和 max(含)之間
支持數據類型:CharSequence,Collection,Map, arrays

@Negative

檢查元素是否嚴格為負數。零值被認為無效。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@NegativeOrZero

檢查元素是否為負或零。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Positive

檢查元素是否嚴格為正。零值被視為無效。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@PositiveOrZero

檢查元素是否為正或零。
支持數據類型:
BigDecimal, BigInteger, byte, short, int, long, 原生類型的封裝類, CharSequence 的任意子類(字元序列表示的數字), Number 的任意子類, javax.money.MonetaryAmount 的任意子類

@Null

檢查值是否為 null
支持數據類型:任何類型

@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 API 在類路徑中,ReadablePartialReadableInstant 的任何實現類

@FutureOrPresent

檢查日期是現在或將來
支持數據類型:同@Future

@Past

檢查日期是否在過去
支持數據類型:同@Future

@PastOrPresent

檢查日期是否在過去或現在
支持數據類型:同@Future

@Pattern(regex=, flags=)

根據給定的 flag 匹配,檢查字元串是否與正則表達式 regex 匹配
支持數據類型:CharSequence

實現示例

@Size

從上文可知,規範中,@Size 支持的數據類型有:CharSequence,Collection,Map, arrays
hibernate-validator 中的實現如下:
file

針對 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();
        }
    }
}

實現邏輯就是按照規範的說明來實現的。

實戰

聲明 Java Bean 約束

可以用以下方式聲明約束:

  1. 欄位級別約束
@NotNull
private String manufacturer;
  1. 屬性級別約束
@NotNull
public String getManufacturer(){
  return manufacturer;
}
  1. 容器級別約束
private Map<@NotNull FuelConsumption, @MaxAllowedFuelConsumption Integer> fuelConsumption = new HashMap<>();
  1. 類級別約束
    在這種情況下,驗證的對象不是單個屬性,而是完整的對象。如果驗證依賴於對象的多個屬性之間的相關性,則類級約束非常有用。
    如:汽車中,乘客數量不能大於座椅數量,否則超載
@ValidPassengerCount
public class Car {

    private int seatCount;

    private List<Person> passengers;

    //...
}
  1. 約束繼承
    當一個類繼承/實現另一個類時,父類聲明的所有約束也會應用在子類繼承的對應屬性上。
    如果方法重寫,約束註解將會聚合,也就是此方法父類和子類聲明的約束都會起作用。

  2. 級聯驗證
    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;
    }
}

此示例指定了三個約束:

  • 任何新創建的 RentalStation 對象都必須滿足 @validRentalStation 約束
  • getCustomers() 返回的客戶列表不能為空,並且必須至少包含 1 個元素
  • getCustomers() 返回的客戶列表不能包含空對象

級聯約束

類似於 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 除了定義分組序列外,還允許重新定義指定類的預設分組。為此,只需將@GroupSequence 添加到類中,併在註解中用指定序列的分組替換 Default 預設分組。

@GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
public class RentalCar extends Car {}

在驗證約束時,直接把其當做預設分組方式來驗證

@GroupSequenceProvider

註意:此為 hibernate-validator 提供,JSR 規範不支持

可用於根據對象狀態動態地重新定義預設分組序列。
需要做兩步:

  1. 實現介面:DefaultGroupSequenceProvider
  2. 在指定類上使用 @GroupSequenceProvider,並指定 value 為上一步的類

示例:

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 調整其行為。
此外,約束註解上還修飾了一些元註解:

  • @Target:指定此註解支持的元素類型,比如:FIELD(屬性)、METHOD(方法)等
  • @Rentention(RUNTIME):指定此類型的註解將在運行時通過反射方式可用
  • @Constraint():標記註解的類型為約束,指定註解所使用的驗證器(寫驗證邏輯的類),如果約束可以用在多種數據類型中,則每種數據類型對應一個驗證器。
  • @Documented:用此註解會被包含在使用方的 JavaDoc 中
  • @Repeatable(List.class):指示註解可以在相同的位置重覆多次,通常具有不同的配置。List 包含註解類型。

驗證器

創建了一個註解,還需要創建一個約束驗證器,以用來驗證使用註解的元素。

需要實現 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 指定了兩個泛型類型:

  1. 第一個是指定需要驗證的註解類
  2. 第二個是指定要驗證的數據類型,當註解支持多種類型時,就要寫多個實現類,並分別指定對應的類型

需要實現兩個方法:

  • 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) {}
}

傳遞 payload 參數給驗證器

目前需要通過 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;
    }
}

message

當違反約束時,應該用到的消息
需要定義一個 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 = "[email protected]";

  @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 統一處理驗證信息。

參考資料

Bean Validation 2.0 (JSR 380)

hibernate-validator 最新版官方資料

hibernate-validator | github
公眾號:逸飛兮(專註於 Java 領域知識的深入學習,從源碼到原理,系統有序的學習)

逸飛兮


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] 1. 數組操作符重載 數組操作符重載 通過重載數組操作符,可以使類的對象支持數組的下標訪問 數組操作符只能重載為類的成員函數 重載函數能且僅能使用一個參數,也就是數組下標 可以定義不同參數的多個重載函數 在重載數組操作符時,要記得數組操作符的原生語義——數組訪問和指針運算。 cpp / ...
  • 從今天起,我會在這裡記錄一下學習深度學習所留下的足跡,目的也很簡單,手頭有近3w個已經標記好正確值得驗證碼,想要從頭訓練出一個可以使用的模型, 雖然我也知道網上的相關模型和demo很多,但是還是非常希望自己可以親手搞一個能用的出來,學習書籍主要是:李金洪老師的《深度學習之Tensorflow 入門、 ...
  • 在做數據分析的過程中,經常會遇到文件的讀取。我想很多人都在這個環節遇到過問題,所以就把自己掌握的一些文件讀取方法記錄下來,以及過程中遇到的一些狀況和解決方法列出來,以便交流。 open open() 函數用於創建或打開指定文件,該函數的語法格式如下: 參數說明: file:表示要創建的文件對象。 f ...
  • 實在不想看JVM了。刷幾道劍指Offer的題,今天就水一水吧,腦子迷糊。 1.二維數組中的查找 在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。 解題思路: ...
  • 一、題目 二、思路 1、dfs 實驗要求用多種思路完成,所以一開始就沿用了上一個實驗馬走棋盤的思路,添加了鄰接矩陣來記錄有向網的權值。總體思路還是DFS遍歷搜索。 過程剪枝: 1、因為要求為最短路徑,而一般情況總會存在多條可行路徑,在判斷過程中需要走過每一條路徑才能知道該路徑的長度,但如果已知一條可 ...
  • [TOC] 閉包函數 什麼是閉包函數 閉包函數把 閉包函數內的變數 + 閉包函數內部的函數, 這兩者包裹起來,然後通過返回值的形式返回出來。 定義在函數的內函數 該函數體代碼包含對該函數外層作用域中變數的引用 函數外層指的不是全局作用域 上述代碼中,f是一個全局的名字,但f拿到了inner的記憶體地址 ...
  • 我是一個2019畢業的非電腦的畢業生,從大二開始喜歡上Java直到現在一直都在學習,Brid從小就對電腦感興趣,可惜高中的時候不懂事,沒有規劃未來,考上了一所專科學院,然後大一併不能轉專業,現在畢業了沒有找到Java應屆的工作,只能找點其他的做,但是這阻住不了我對Java的喜歡,趁現在工作的晚上 ...
  • “容器”這兩個字很少被 Python 技術文章提起。一看到“容器”,大家想到的多是那頭藍色小鯨魚:Docker,但這篇文章和它沒有任何關係。本文里的容器,是 Python 中的一個抽象概念,是對專門用來裝其他對象的數據類型的統稱。 在 Python 中,有四類最常見的內建容器類型: 列表(list) ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...