天天看點

SpringBoot分組校驗及自定義校驗注解

SpringBoot分組校驗及自定義校驗注解

前言

  在日常的開發中,參數校驗是非常重要的一個環節,嚴格參數校驗會減少很多出bug的機率,增加接口的安全性。在此之前寫過一篇

SpringBoot統一參數校驗

主要介紹了一些簡單的校驗方法。而這篇則是介紹一些進階的校驗方式。比如說:在某個接口編寫的過程中肯定會遇到,當xxType值為A,paramA值必傳。xxType值為B,paramB值必須傳。對于這樣的,通常的做法就是在controller加上各種if判斷。顯然這樣的代碼是不夠優雅的,而分組校驗及自定義參數校驗,就是來解決這個問題的。

PathVariable參數校驗

  Restful的接口,在現在來講應該是比較常見的了,常用的位址欄的參數,我們都是這樣校驗的。

/**
 * 擷取電話号碼資訊
 */
@GetMapping("/phoneInfo/{phone}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    // 驗證電話号碼是否有效
    String pattern = "^[1][3,4,5,7,8][0-9]{9}$";
    boolean isValid =  Pattern.matches(pattern, phone);
    if(isValid){
        // 執行相應邏輯
        return ResultVoUtil.success(phone);
    } else {
        // 傳回錯誤資訊
        return ResultVoUtil.error("手機号碼無效");
    }
}           

很顯然上面的代碼不夠優雅,是以我們可以在參數後面,添加對應的正規表達式

phone:正規表達式

來進行驗證。這樣就省去了在controller編寫校驗代碼了。

/**
 * 擷取電話号碼資訊
 */
@GetMapping("/phoneInfo/{phone:^[1][3,4,5,7,8][0-9]{9}$}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    return ResultVoUtil.success(phone);
}           

雖然這樣處理後代碼更精簡了。但是如果傳入的手機号碼,不符合規則會直接傳回404。而不是提示手機号碼錯誤。錯誤資訊如下:

SpringBoot分組校驗及自定義校驗注解

自定義校驗注解

  我們以校驗手機号碼為例,雖然

validation

提供了

@Pattern

這個注解來使用正規表達式進行校驗。如果被使用在多處,一旦正規表達式發生更改,則需要一個一個的進行修改。很顯然為了避免做這樣的無用功,

自定義校驗注解

就是你的好幫手。

@Data
public class PhoneForm {

    /**
     * 電話号碼
     */
    @Pattern(regexp = "^[1][3,4,5,7,8][0-9]{9}$" , message = "電話号碼有誤")
    private String phone;

}           

  要實作一個自定義校驗注解,主要是有兩步。一是注解本身,二是校驗邏輯實作類。

PhoneVerify 校驗注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
 
    String message() default "手機号碼格式有誤";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}           

PhoneValidator 校驗實作類

public class PhoneValidator implements ConstraintValidator<Phone, Object> {

    @Override
    public boolean isValid(Object telephone, ConstraintValidatorContext constraintValidatorContext) {
        String pattern = "^1[3|4|5|7|8]\\d{9}$";
        return Pattern.matches(pattern, telephone.toString());
    }
}           

CustomForm 表單資料

@Data
public class CustomForm {

    /**
     * 電話号碼
     */
    @Phone
    private String phone;

}           

測試接口

@PostMapping("/customTest")
public ResultVo customTest(@RequestBody @Validated CustomForm form){
    return ResultVoUtil.success(form.getPhone());
}           

注解的含義

@Target({ElementType.FIELD})

  注解是指定目前自定義注解可以使用在哪些地方,這裡僅僅讓他可以使用屬性上。但還可以使用在更多的地方,比如說方法上、構造器上等等。

  • TYPE - 類,接口(包括注解類型)或枚舉
  • FIELD - 字段(包括枚舉常量)
  • METHOD - 方法
  • PARAMETER - 參數
  • CONSTRUCTOR - 構造函數
  • LOCAL_VARIABLE - 局部變量
  • ANNOTATION_TYPE -注解類型
  • PACKAGE - 包
  • TYPE_PARAMETER - 類型參數
  • TYPE_USE - 使用類型
@Retention(RetentionPolicy.RUNTIME)

  指定目前注解保留到運作時。保留政策有下面三種:

  • SOURCE - 注解隻保留在源檔案,當Java檔案編譯成class檔案的時候,注解被遺棄。
  • CLASS - 注解被保留到class檔案,但jvm加載class檔案時候被遺棄,這是預設的生命周期。
  • RUNTIME - 注解不僅被儲存到class檔案中,jvm加載class檔案之後,仍然存在。
@Constraint(validatedBy = PhoneValidator.class)

  指定了目前注解使用哪個校驗類來進行校驗。

分組校驗

UserForm

@Data
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增時id必須為空", groups = {Insert.class})
    @NotNull(message = "更新時id不能為空", groups = {Update.class})
    private String id;

    /**
     * 類型
     */
    @NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
    private String name;

    /**
     * 年齡
     */
    @NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
    private String age;
    
}           

Insert分組

public interface Insert {
}           

Update分組

public interface Update {
}           

/**
 * 添加使用者
 */
@PostMapping("/addUser")
public ResultVo addUser(@RequestBody @Validated({Insert.class}) UserForm form){
      // 選擇對應的分組進行校驗
    return ResultVoUtil.success(form);
}

/**
 * 更新使用者
 */
@PostMapping("/updateUser")
public ResultVo updateUser(@RequestBody @Validated({Update.class}) UserForm form){
    // 選擇對應的分組進行校驗
    return ResultVoUtil.success(form);
}           

測試結果

添加測試
SpringBoot分組校驗及自定義校驗注解
更新測試
SpringBoot分組校驗及自定義校驗注解

順序校驗

@GroupSequence

  在

@GroupSequence

内可以指定,分組校驗的順序。比如說

@GroupSequence({Insert.class, Update.class, UserForm.class})

先執行

Insert

校驗,然後執行

Update

校驗。如果

Insert

分組,校驗失敗了,則不會進行

Update

分組的校驗。

@Data
@GroupSequence({Insert.class, Update.class, UserForm.class})
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增時id必須為空", groups = {Insert.class})
    @NotNull(message = "更新時id不能為空", groups = {Update.class})
    private String id;

    /**
     * 類型
     */
    @NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
    private String name;

    /**
     * 年齡
     */
    @NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
    private String age;

}           
/**
* 編輯使用者
*/
@PostMapping("/editUser")
public ResultVo editUser(@RequestBody @Validated UserForm form){
    return ResultVoUtil.success(form);
}           

  哈哈哈,測試結果其實是個死循環,不管你咋輸入都會報錯,小夥伴可以嘗試一下哦。上面的例子隻是個示範,在實際中還是别這樣做了,需要根據具體邏輯進行校驗。

自定義分組校驗

  對于之前提到了當xxType值為A,paramA值必傳。xxType值為B,paramB值必須傳這樣的場景。單獨使用分組校驗和分組序列是無法實作的。需要使用

@GroupSequenceProvider

才行。

自定義分組表單

@Data
@GroupSequenceProvider(value = CustomSequenceProvider.class)
public class CustomGroupForm {

    /**
     * 類型
     */
    @Pattern(regexp = "[A|B]" , message = "類型不必須為 A|B")
    private String type;

    /**
     * 參數A
     */
    @NotEmpty(message = "參數A不能為空" , groups = {WhenTypeIsA.class})
    private String paramA;

    /**
     * 參數B
     */
    @NotEmpty(message = "參數B不能為空", groups = {WhenTypeIsB.class})
    private String paramB;

    /**
     * 分組A
     */
    public interface WhenTypeIsA {

    }

    /**
     * 分組B
     */
    public interface WhenTypeIsB {

    }

}           

CustomSequenceProvider

public class CustomSequenceProvider implements DefaultGroupSequenceProvider<CustomGroupForm> {

    @Override
    public List<Class<?>> getValidationGroups(CustomGroupForm form) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();

        defaultGroupSequence.add(CustomGroupForm.class);

        if (form != null && "A".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class);
        }

        if (form != null && "B".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class);
        }

        return defaultGroupSequence;
    }
}           

/**
 * 自定義分組
 */
@PostMapping("/customGroup")
public ResultVo customGroup(@RequestBody @Validated CustomGroupForm form){
    return ResultVoUtil.success(form);
}           

Type類型為A
SpringBoot分組校驗及自定義校驗注解
Type類型為B
SpringBoot分組校驗及自定義校驗注解

小結一下

  

GroupSequence

注解是一個标準的Bean認證注解。正如之前,它能夠讓你靜态的重新定義一個類的,預設校驗組順序。然而

GroupSequenceProvider

它能夠讓你動态的定義一個校驗組的順序。

注意的一個點

SpringBoot 2.3.x 移除了

validation

依賴需要手動引入依賴。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>           

總結

  個人的一些小經驗,參數的非空判斷,這個應該是校驗的第一步了,除了非空校驗,我們還需要做到下面這幾點:

  • 普通參數 - 需要限定字段的長度。如果會将資料存入資料庫,長度以資料庫為準,反之根據業務确定。
  • 類型參數 - 最好使用正則對可能出現的類型做到嚴格校驗。比如

    type

    的值是【0|1|2】這樣的。
  • 清單(list)參數 - 不僅需要對list内的參數是否合格進行校驗,還需要對list的size進行限制。比如說 100。
  • 日期,郵件,金額,URL這類參數都需要使用對于的正則進行校驗。
  • 參數真實性 - 這個主要針對于 各種

    Id

    比如說

    userId

    merchantId

    ,對于這樣的參數,都需要進行真實性校驗,判斷系統内是有含有,并且對應的狀态是否正常。

  參數校驗越嚴格越好,嚴格的校驗規則不僅能減少接口出錯的機率,同時還能避免出現髒資料,進而來保證系統的安全性和穩定性。

錯誤的提醒資訊需要友好一點哦,防止等下被前端大哥吐槽哦。

上期回顧

結尾

  如果覺得對你有幫助,可以多多評論,多多點贊哦,也可以到我的首頁看看,說不定有你喜歡的文章,也可以随手點個關注哦,謝謝。

  我是不一樣的科技宅,每天進步一點點,體驗不一樣的生活。我們下期見!