【深度思考】如何優雅的校驗引數?

申城異鄉人發表於2022-12-22

在日常的開發工作中,為了保證落庫資料的完整性,引數校驗絕對是必不可少的一部分,本篇文章就來講解下在專案中該如何優雅的校驗引數。

假設有一個新增學員的介面,一般第一步我們都會先校驗學員資訊是否正確,然後才會落庫,簡單起見,假設新增學員時只有2個欄位:姓名、年齡。

@Data
public class StudentVO {
    /**
     * 姓名
     */
    private String name;

    /**
     * 年齡
     */
    private Integer age;
}

要求為:姓名和年齡必填,姓名不能超過20個字元。

1. 最原始的寫法

先來看下最原始的寫法,相信大多數人都這麼寫過,或者說在初學Java時都這麼寫過:

public String validateStudentVO(StudentVO studentVO) {
    if (StringUtils.isBlank(studentVO.getName())) {
        return "姓名不能為空";
    }
    if (studentVO.getName().length() > 20) {
        return "姓名不能超過20個字元";
    }
    if (studentVO.getAge() == null) {
        return "年齡不能為空";
    }

    return null;
}

這麼寫最好理解,但一般一個專案中都會有很多介面,如果都這麼寫的話,重複程式碼會非常多,顯得非常臃腫,而且對於一個工作多年的開發來說,如果每天都寫這樣的程式碼,會覺得特別沒有技術含量。

2. Bean Validation

既然有需求場景,就會有規範,這個規範就是Bean Validation,官網地址是 https://beanvalidation.org/

Bean Validation先後經歷了1.0(JSR 303)、1.1(JSR 349)、2.0(JSR 380)這3個版本,目前專案中使用比較多的是Bean Validation 2.0,本篇文章講解的內容也是基於Bean Validation 2.0版本。

Bean Validation 2.0之後,現在改名叫Jakarta Bean Validation了。

pom依賴座標如下所示:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

不過從2.0.1.Final之後的版本依賴都改為了jakarta.validation-api:

新版本pom依賴座標如下所示:

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>2.0.2</version>
</dependency>

3. Hibernate Validator

Hibernate Validator是 Bean Validation 的參考實現 ,不僅提供了規範中所有內建constraint的實現,除此之外還提供了一些附加的 constraint。

pom依賴座標如下所示:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
</dependency>

因為hibernate-validator中已經包含了validation-api,因此專案中如果引入了hibernate-validator,就沒必要重複引入validation-api了:

image-20221221144308018

4. Bean Validation 2.0原生註解

Bean Validation 2.0中包含了22個註解,如下圖所示:

接下來詳細講解下這22個註解的用途。

4.1 @AssertTrue

作用:被標記的元素必須為true。

支援的Java型別:boolean、Boolean。

使用示例:

@AssertTrue
private Boolean newStudent;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setNewStudent(false);

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

snipaste_20221219_162307

上面輸出的message是預設的,在實際使用時可以自定義:

@AssertTrue(message = "newStudent必須為true")
private Boolean newStudent;

效果如下圖所示:

snipaste_20221219_164946

注意事項:

1)@AssertTrue註解識別不了欄位值為null的場景:

snipaste_20221219_163148

2)如果將@AssertTrue註解使用在boolean、Boolean之外的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常:

@AssertTrue
private String name;

4.2 @AssertFalse

作用:被標記的元素值必須為false。

其餘的和@AssertTrue註解一致。

使用示例:

@AssertFalse(message = "newStudent必須為false")
private Boolean newStudent;

4.3 @DecimalMax

作用:被標記的元素必須小於或等於指定的值。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String。

使用示例:

@DecimalMax(value = "30000")
private BigDecimal balance;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setBalance(new BigDecimal("30001"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

上面輸出的message是預設的,在實際使用時可以自定義:

@DecimalMax(value = "30000", message = "賬戶餘額必須小於或等於30000")
private BigDecimal balance;

效果如下圖所示:

snipaste_20221219_180912

注意事項:

1)@DecimalMax註解識別不了欄位值為null的場景:

2)如果將@DecimalMax註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常:

@DecimalMax(value = "30000", message = "賬戶餘額必須小於或等於30000")
private Boolean newStudent;

4.4 @DecimalMin

作用:被標記的元素值必須大於或等於指定的值。

其餘的和@DecimalMax註解一致。

使用示例:

@DecimalMin(value = "5000", message = "充值餘額必須大於或等於5000")
private BigDecimal rechargeAmount;

4.5 @Digits

作用:被標記的元素整數位數和小數位數必須小於或等於指定的值。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String。

使用示例:

@Digits(integer = 6, fraction = 2)
private BigDecimal rechargeAmount;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("100000.999"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

上面輸出的message是預設的,在實際使用時可以自定義:

@Digits(integer = 6, fraction = 2, message = "充值金額只允許6位整數、2位小數")
private BigDecimal rechargeAmount;

效果如下圖所示:

注意事項:

1)@Digits註解識別不了欄位值為null的場景:

2)如果將@Digits註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常:

@Digits(integer = 6, fraction = 2, message = "充值金額只允許6位整數、2位小數")
private Boolean newStudent;

4.6 @Email

作用:被標記的元素必須是郵箱地址。

支援的Java型別:String。

使用示例:

@Email
private String email;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setEmail("活著");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220140414799

上面輸出的message是預設的,在實際使用時可以自定義:

@Email(message = "無效的電子郵件地址")
private String email;

效果如下圖所示:

image-20221220141623447

注意事項:

1)@Email註解識別不了欄位值為null或空字串""的場景:

image-20221220142325648

2)如果將@Email註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.7 @Future

作用:被標記的元素必須為當前時間之後。

支援的Java型別:Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime等。

使用示例:

@Future
private Date startingDate;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setStartingDate(new Date());

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220143841736

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Future(message = "必須是一個將來的時間")
private Date startingDate;

2)@Future註解識別不了欄位值為null的場景。

3)如果將@Future註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.8 @FutureOrPresent

作用:被標記的元素必須為當前時間或之後。

支援的Java型別:Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime等。

使用示例:

@FutureOrPresent
private Date startingDate;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setStartingDate(DateUtils.addMilliseconds(new Date(), 1));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220145520752

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@FutureOrPresent(message = "必須是一個將來或現在的時間")
private Date startingDate;

2)@FutureOrPresent註解識別不了欄位值為null的場景。

3)如果將@FutureOrPresent註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.9 @Past

作用:被標記的元素必須為當前時間之前。

支援的Java型別:Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime等。

使用示例:

@Past
private Date latestAttendanceTime;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setLatestAttendanceTime(DateUtils.addMinutes(new Date(), 10));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220150626760

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Past(message = "必須是一個過去的時間")
private Date latestAttendanceTime;

2)@Past註解識別不了欄位值為null的場景。

3)如果將@Past註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.10 @PastOrPresent

作用:被標記的元素必須為當前時間或之前。

支援的Java型別:Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime等。

使用示例:

@PastOrPresent
private Date latestAttendanceTime;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setLatestAttendanceTime(DateUtils.addMinutes(new Date(), 10));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220151459339

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@PastOrPresent(message = "必須是一個過去或現在的時間")
private Date latestAttendanceTime;

2)@PastOrPresent註解識別不了欄位值為null的場景。

3)如果將@PastOrPresent註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.11 @Max

作用:被標記的元素必須小於或等於指定的值。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String。

使用示例:

@Max(value = 10000)
private BigDecimal balance;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setBalance(new BigDecimal("10000.01"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220152359301

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Max(value = 10000, message = "必須小於或等於10000")
private BigDecimal balance;

2)@Max註解識別不了欄位值為null的場景。

3)如果將@Max註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.12 @Min

作用:被標記的元素必須大於或等於指定的值。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String。

使用示例:

@Min(value = 5000)
private BigDecimal rechargeAmount;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("4999"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220155849229

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Min(value = 5000, message = "必須大於或等於5000")
private BigDecimal rechargeAmount;

2)@Min註解識別不了欄位值為null的場景。

3)如果將@Min註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.13 @Negative

作用:被標記的元素必須是負數。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、

double、Double。

使用示例:

@Negative
private BigDecimal rechargeAmount;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("0"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220171024124

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Negative(message = "金額必須是負數")
private BigDecimal rechargeAmount;

2)@Negative註解識別不了欄位值為null的場景。

3)如果將@Negative註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.14 @NegativeOrZero

@NegativeOrZero註解和@Negative註解基本一致,唯一的區別是被標記的元素除了可以是負數,也可以是零。

使用示例:

@NegativeOrZero(message = "金額必須是負數或零")
private BigDecimal rechargeAmount;

4.15 @Positive

作用:被標記的元素必須是正數。

支援的Java型別:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、

double、Double。

使用示例:

@Positive
private BigDecimal rechargeAmount;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("0"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220173146103

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Positive(message = "充值金額必須是正數")
private BigDecimal rechargeAmount;

2)@Positive註解識別不了欄位值為null的場景。

3)如果將@Positive註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.16 @PositiveOrZero

@PositiveOrZero註解和@Positive註解基本一致,唯一的區別是被標記的元素除了可以是正數,也可以是零。

使用示例:

@PositiveOrZero(message = "充值金額必須是正數或零")
private BigDecimal rechargeAmount;

4.17 @Null

作用:被標記的元素必須為null。

支援的Java型別:Object。

使用示例:

@Null
private String namePinYin;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setNamePinYin("zhangsan");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220174738949

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Null(message = "姓名拼音必須為null")
private String namePinYin;

4.18 @NotNull

作用:被標記的元素必須不為null。

其餘和@Null註解一致。

4.19 @NotEmpty

作用:被標記的元素不為null,且不為空(字串的話,就是length要大於0,集合的話,就是size要大於0)。

支援的Java型別:String、Collection、Map、Array。

使用示例:

/**
 * 姓名
 */
@NotEmpty
private String name;

/**
 * 家長資訊
 */
@NotEmpty
private List<ParentVO> parentVOList;

ParentVO如下所示:

@Data
public class ParentVO {
    /**
     * 姓名
     */
    @NotEmpty(message = "姓名不能為空")
    private String name;

    /**
     * 手機號
     */
    @NotEmpty(message = "手機號不能為空")
    private String mobile;
}

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setName("");
studentVO.setParentVOList(new ArrayList<>());

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220181939012

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@NotEmpty(message = "姓名不能為空")
private String name;

2)如果將@NotEmpty註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

3)巢狀驗證問題

簡單修改下上面的驗證程式碼:

StudentVO studentVO = new StudentVO();
studentVO.setName("張三");

ParentVO parentVO = new ParentVO();
studentVO.setParentVOList(Lists.newArrayList(parentVO));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

此時的輸出結果如下所示:

image-20221220183222130

從輸出結果可以看出,StudentVO裡增加的@NotEmpty註解生效了,但巢狀的ParentVO裡的校驗註解並未生效,如果想生效的話,需要加上@Valid註解:

/**
 * 家長資訊
 */
@Valid
@NotEmpty
private List<ParentVO> parentVOList;

再次執行上面的驗證程式碼,輸出結果如下圖所示:

image-20221220183913587

可以看出,巢狀的ParentVO裡的校驗註解也生效了。

4.20 @NotBlank

作用:被標記的元素不為null,且必須有一個非空格字元。

這裡提下和@NotEmpty的區別,

作用於字串的話,@NotEmpty能校驗出null、”“這2種場景,而@NotBlank能校驗出null、”“、” “這3種場景,

作用於集合的話,@NotEmpty支援,但@NotBlank不支援。

支援的Java型別:String。

使用示例:

@NotBlank
private String name;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setName(" ");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221220185653073

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@NotBlank(message = "姓名不能為空")
private String name;

2)如果將@NotBlank註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.21 @Size

作用:被標記的元素長度/大小必須在指定的範圍內(字串的話,就是length要在指定的範圍內,集合的話,就是size要在指定的範圍內)。

支援的Java型別:String、Collection、Map、Array。

使用示例:

@Size(min = 2, max = 5)
private String name;

@Size(min = 1, max = 5)
private List<ParentVO> parentVOList;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setName("張三李四王五");
studentVO.setParentVOList(new ArrayList<>());

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221221103331170

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Size(min = 2, max = 5, message = "姓名不能少於2個字元,不能多於5個字元")
private String name;

@Size(min = 1, max = 5, message = "至少新增一位家長資訊,最多不能超過5位")
private List<ParentVO> parentVOList;

2)@Size註解識別不了欄位值為null的場景。

2)如果將@Size註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4.22 @Pattern

作用:被標記的元素必須匹配指定的正規表示式。

支援的Java型別:String。

使用示例:

@Pattern(regexp = "^[1-9]\\d{5}$")
private String postcode;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setPostcode("2000001");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221221105001625

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Pattern(regexp = "^[1-9]\\d{5}$", message = "郵政編碼格式錯誤")
private String postcode;

2)@Pattern註解識別不了欄位值為null的場景。

3)如果將@Pattern註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

5. Hibernate Validator擴充套件註解

Hibernate Validator除了支援上面提到的22個原生註解外,還擴充套件了一些註解:

image-20221221145450366

接下來詳細講解幾個常用的。

5.1 @Length

作用:被標記的元素必須在指定的長度範圍內。

支援的Java型別:String。

使用示例:

@Length(min = 2, max = 5)
private String name;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setName("張三李四王五");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221221110257679

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Length(min = 2, max = 5, message = "姓名不能少於2個字元,不能多於5個字元")
private String name;

2)@Length註解識別不了欄位值為null的場景。

3)如果將@Length註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

5.2 @Range

@Range註解相當於同時融合了@Min註解和@Max註解的功能,如下圖所示:

image-20221221113851165

因此它的作用是:被註解的元素必須大於或等於指定的最小值,小於或等於指定的最大值。

它支援的Java型別也和@Min註解和@Max註解一致:

BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String。

使用示例:

@Range(min = 1000L, max = 10000L)
private BigDecimal rechargeAmount;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("500"));

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221221112728704

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@Range(min = 1000L, max = 10000L, message = "至少充值1000,最多充值10000")
private BigDecimal rechargeAmount;

2)@Range註解識別不了欄位值為null的場景。

3)如果將@Range註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

4)不建議將@Range註解使用在String型別上。

5.3 @URL

作用:被標記的元素必須是一個有效的url地址。

它的內部其實是使用了@Pattern註解,如下圖所示:

image-20221221115137250

因此它支援的Java型別和@Pattern註解一致:String。

使用示例:

@URL
private String url;

驗證:

StudentVO studentVO = new StudentVO();
studentVO.setRechargeAmount(new BigDecimal("1000"));
studentVO.setUrl("url地址");

ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();

Set<ConstraintViolation<StudentVO>> constraintViolations = validator.validate(studentVO);
for (ConstraintViolation<StudentVO> constraintViolation : constraintViolations) {
    System.out.println(constraintViolation.getMessage());
}

輸出結果:

image-20221221114343660

注意事項:

1)上面輸出的message是預設的,在實際使用時可以自定義:

@URL(message = "無效的url地址")
private String url;

2)@URL註解識別不了欄位值為null的場景。

3)如果將@URL註解使用在不支援的Java型別,程式會丟擲javax.validation.UnexpectedTypeException異常。

6. Spring Web專案

如果專案本身是基於Spring Web的,可以使用@ControllerAdvice+@ExceptionHandler來全域性處理引數校驗。

首先,新建一個全域性異常處理器,並新增@RestControllerAdvice註解:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
}

說明:因為介面返回的是json,這裡使用@RestControllerAdvice等價於同時使用了@ControllerAdvice@ResponseBody

接著,我們將文初的StudentVO修改為:

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
public class StudentVO {
    /**
     * 姓名
     */
    @NotBlank(message = "姓名不能為空")
    @Length(max = 20, message = "姓名不能超過20個字元")
    private String name;

    /**
     * 年齡
     */
    @NotNull(message = "年齡不能為空")
    private Integer age;
}

然後在api介面的引數前增加@Valid註解:

@RestController
public class StudentController {
    @Autowired
    private StudentService studentService;

    @PostMapping("student/add")
    public CommonResponse<Void> add(@RequestBody @Valid StudentVO studentVO) {
        studentService.add(studentVO);

        return CommonResponse.success();
    }
}

6.1 處理MethodArgumentNotValidException異常

在全域性異常處理器中新增MethodArgumentNotValidException異常處理邏輯:

/**
 * 處理MethodArgumentNotValidException
 *
 * @param e
 * @return
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    log.error("方法引數不正確", e);

    return CommonResponse.error(HttpStatus.BAD_REQUEST.value(),
            "引數錯誤:" + e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
}

最後使用postman呼叫介面進行驗證,如下圖所示:

snipaste_20221111_144615

從介面返回結果,可以看出,全域性異常處理器成功的處理了MethodArgumentNotValidException異常的邏輯,因為上面呼叫介面,其實程式是丟擲了org.springframework.web.bind.MethodArgumentNotValidException異常,不過因為在全域性異常處理器中定義了該異常的處理邏輯,所以程式按照定義的格式返回給了前端,而不是直接將異常拋給前端:

snipaste_20221111_145918

6.2 處理HttpMessageNotReadableException異常

上面的介面,如果我們不傳引數,程式會丟擲org.springframework.http.converter.HttpMessageNotReadableException異常,如下圖所示:

snipaste_20221111_151244

因此需要在全域性異常處理器中新增HttpMessageNotReadableException異常處理邏輯:

/**
 * 處理HttpMessageNotReadableException
 *
 * @param e
 * @return
 */
@ExceptionHandler(HttpMessageNotReadableException.class)
public CommonResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
    log.error("引數錯誤", e);

    return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), "引數錯誤");
}

使用postman呼叫介面進行驗證,如下圖所示:

snipaste_20221111_151748

6.3 處理MissingServletRequestParameterException異常

假設我們有一個根據名字查詢學員的GET請求的介面:

@GetMapping("student/get")
public CommonResponse<StudentVO> get(@RequestParam String name) {
    StudentVO studentVO = studentService.getByName(name);

    return CommonResponse.success(studentVO);
}

但呼叫時,我們不傳遞引數name,程式會丟擲org.springframework.web.bind.MissingServletRequestParameterException異常,如下圖所示:

snipaste_20221111_164107

因此需要在全域性異常處理器中新增MissingServletRequestParameterException異常處理邏輯:

/**
 * 處理MissingServletRequestParameterException
 *
 * @param e
 * @return
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
public CommonResponse<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
    log.error("引數錯誤", e);

    return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), "引數錯誤");
}

使用postman呼叫介面進行驗證,如下圖所示:

snipaste_20221111_164216

6.4 處理ConstraintViolationException異常

還是上面的查詢學員介面,不僅要傳引數name,還得保證引數name不能是個空字串,因此需要在引數前加上@NotBlank註解:

@GetMapping("student/get")
public CommonResponse<StudentVO> get(@RequestParam @NotBlank(message = "名字不能為空") String name) {
    StudentVO studentVO = studentService.getByName(name);

    return CommonResponse.success(studentVO);
}

並且需要在控制器Controller上新增@Validated註解:

snipaste_20221111_165935

注意事項:控制器上的@Validated註解一定要新增,否則引數上加的@NotBlank註解不會生效。

此時呼叫介面,但引數name傳遞個空字串,程式會丟擲javax.validation.ConstraintViolationException異常,如下圖所示:

snipaste_20221111_165205

因此需要在全域性異常處理器中新增ConstraintViolationException異常處理邏輯:

/**
 * 處理ConstraintViolationException
 *
 * @param e
 * @return
 */
@ExceptionHandler(ConstraintViolationException.class)
public CommonResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
    log.error("引數錯誤", e);

    return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), e.getConstraintViolations().iterator().next().getMessage());
}

使用postman呼叫介面進行驗證,如下圖所示:

snipaste_20221111_170610

6.5 擴充套件

全域性異常處理器除了處理上面提到的4個引數校驗的異常,一般也會處理業務上丟擲的異常,如Service層丟擲的自定義異常:

@Service
public class StudentService {
    public StudentVO getByName(String name) {
        throw new ServiceException("學員不存在");
    }
}
/**
 * 業務異常
 */
public class ServiceException extends RuntimeException {
    public ServiceException(String message) {
        super(message);
    }
}

所以一般全域性異常處理器中都有處理ServiceException的邏輯:

/**
 * 處理ServiceException
 *
 * @param e
 * @return
 */
@ExceptionHandler(ServiceException.class)
public CommonResponse<Void> handleServiceException(ServiceException e) {
    log.error("業務異常", e);

    return CommonResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
}

因為異常有很多種型別,而本文中提到的只是其中的幾個,因此為了起到兜底作用,可以在全域性異常處理器中新增處理Exception異常的邏輯,當程式丟擲未知的異常時,可以統一處理,返回某個固定的提示給前端:

/**
 * 處理Exception
 *
 * @param e
 * @return
 */
@ExceptionHandler(Exception.class)
public CommonResponse<Void> handleException(Exception e) {
    log.error("系統異常", e);

    return CommonResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "操作失敗,請稍後重試");
}

6.6 完整的GlobalExceptionHandler程式碼

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

/**
 * 全域性異常處理器
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 處理MethodArgumentNotValidException
     *
     * @param e
     * @return
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResponse<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("方法引數不正確", e);

        return CommonResponse.error(HttpStatus.BAD_REQUEST.value(),
                "引數錯誤:" + e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
    }

    /**
     * 處理HttpMessageNotReadableException
     *
     * @param e
     * @return
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public CommonResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error("引數錯誤", e);

        return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), "引數錯誤");
    }

    /**
     * 處理MissingServletRequestParameterException
     *
     * @param e
     * @return
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public CommonResponse<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
        log.error("引數錯誤", e);

        return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), "引數錯誤");
    }

    /**
     * 處理ConstraintViolationException
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public CommonResponse<Void> handleConstraintViolationException(ConstraintViolationException e) {
        log.error("引數錯誤", e);

        return CommonResponse.error(HttpStatus.BAD_REQUEST.value(), e.getConstraintViolations().iterator().next().getMessage());
    }

    /**
     * 處理ServiceException
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ServiceException.class)
    public CommonResponse<Void> handleServiceException(ServiceException e) {
        log.error("業務異常", e);

        return CommonResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
    }

    /**
     * 處理Exception
     *
     * @param e
     * @return
     */
    @ExceptionHandler(Exception.class)
    public CommonResponse<Void> handleException(Exception e) {
        log.error("系統異常", e);

        return CommonResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "操作失敗,請稍後重試");
    }
}

相關文章