SpringBoot如何優雅的進行引數校驗

xiezhr發表於2024-03-25

寫在前面

上一篇文章中我們學會了如何優雅的接收前端引數,傳送門

SpringBoot如何優雅的接收前端引數

接收到引數後,接下來要做的就是校驗引數的合法性。這一步的重要性就不用多說了。

即使前端已經對資料進行了校驗,我們後端還是要再對接收到的資料進行一遍徹底的校驗。

這樣可以避免張三等人利用Http工具,繞過瀏覽器非法請求資料。

廢話不多說,看完這篇文章,你將從繁瑣的校驗邏輯中解脫出來

一、傳統引數校驗

雖然往事不堪回首,但還是得回憶一下我們傳統引數校驗的痛點。

下面是我們傳統校驗使用者名稱和郵箱是否合法的程式碼

if (username == null || username.isEmpty()) {
    throw new IllegalArgumentException("使用者名稱不能為空");
}

if (isValidEmail(email)) {
    throw new IllegalArgumentException("郵箱格式不正確");
}

public boolean isValidEmail(String email) {
    String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
    Pattern pattern = Pattern.compile(emailRegex);
    Matcher matcher = pattern.matcher(email);
    return matcher.matches();
}

這樣的程式碼不僅冗長,而且難以維護,尤其是在多個地方重複使用時,容易出錯。

面對上面的痛點,我們就得解放雙手,利用框架來完成校驗。

它只需要透過簡單的註解來定義校驗規則,讓框架來幫助我們處理校驗邏輯,讓我們程式碼變得更加的優雅。

二、幾個名詞

問題①:JSR是什麼?

JSR(Java Specification Requests) 是一套 JavaBean 引數校驗的標準,它定義了很多常用的校驗註解。

我們可以直接將這些註解加在我們 JavaBean 的屬性上面,這樣就可以在需要校驗的時候進行校驗了,非常方便!

問題②:Bean Validation是什麼?

Bean Validation是一個抽象的框架,它定義了驗證規則,而不會涉及具體的業務邏輯

問題③:Hibernate Validator是什麼?

Bean Validation的實現,目前最新版的 Hibernate Validator 6.xBean Validation 2.0(JSR 380)的參考實現

三、所需依賴

Spring boot 2.3以前版本,Springbootspring-boot-starter-web預設內建了Hibernate-Validator

這些版本直接引入spring-boot-starter-web即可,後面的版本需要單獨引入

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

在後面的測試中會用到lombokSpringBootwebtest等基礎依賴,這裡就不一一給出

四、註解及作用

註解 作用型別 作用
@NotBlank(message='') 字串 被註釋字串非null,且長度必須大於0
@NotEmpty 字串 被註釋的字串必須非空
@NotNull 任意 被註釋的元素不能為null
@Null 任意 被註釋的元素必須為null
@Email 字串 被註釋的元素必須是電子郵箱地址
@AssertTrue 布林值 被註釋的元素必須為true
@AssertFalse 布林值 被註釋的元素必須為false
@Max(value = , message = "") 數字 被註釋的元素必須是一個數字,且小於或等於最大值
@Min(value = , message = "") 數字 被註釋的元素必須是一個數字,且大於或等於最小值
@DecimalMax(value = "", message = "") 數字 被註釋的元素必須是一個數字,且小於或等於最大值
@DecimalMin(value = "", message = "") 數字 被註釋的元素必須是一個數字,且大於或等於最小值
@Pattern(regex=,flag=) 字串 被註釋的元素是否符合正規表示式規則
@Size(max=, min=) 數字 被註釋的元素的大小必須在指定範圍內,min 表示最小,max表示最大
@Digits (integer, fraction) 數字 被註釋的元素必須是一個數字,且在可接收範圍內
@Positive 數字 被註釋的元素必須是正數
@PositiveOrZero 數字 被註釋的元素必須是0或正數
@Negative 數字 被註釋的元素必須是負數
@NegativeOrZero 數字 被註釋的元素必須是0或者負數
@Past 日期 被註釋的元素必須是一個過去的日期
@PastOrPresent 日期 被註釋的元素必須是一個過去或當前日期
@Future 日期 被註釋的元素必須是一個過去的日期
@FutureOrPresent 日期 被註釋的元素必須是一個將來或當期的日期

看到這些註解後,大家可能會對【@NotNul@NotEmpty@NotBlank】這三個註解有點不理解,這裡稍作解釋

  • @NotNull:任何物件的value不能為null。
  • @NotEmpty:集合物件的元素不為0,即集合不為空,也可以用於字串不為null。
  • @NotBlank:只能用於字串不為null,並且字串trim()以後length要大於0。

五、快速入門

5.1 新增加一個一個User 實體類

@Data
public class User {
    //姓名
    @NotBlank(message = "使用者名稱不能為空")  //註解確保姓名不為空
    private  String name;

    //性別
    @NotBlank(message = "性別不能為空")   //註解確保性別不為空
    private String sex;

    //年齡
    @NotNull(message = "年齡不能為空")  //註解確保年齡不為空
    @Max(value = 120,message = "年齡不能大於120")  //註解確保年齡必須小於等於120
    @Min(value = 18,message = "年齡不能小於18")   //註解確保年齡必須大於等於18
    private  Integer age;

    //郵箱
    @Email(message = "郵箱格式不正確")    //註解確保郵箱格式正確
    @NotBlank(message = "郵箱不能為空")
    private String email;
}

上述程式碼說明:

  • @NotBlank: 此註解確保字串不為空並且不能為空字串,且去掉前後空格後的長度必須大於 0。它常用於字串欄位驗證。message 屬性用於指定提示資訊;
  • @NotNull: 此註解確保整數型別不能為 null
  • @Min@Max: 這兩個註解用於驗證數字值是否在指定的範圍內。例如,在上面的示例中,我們想要確保 age 的值在 18 到 120 之間;
  • @Email: 此註解用於驗證字串值是否是有效的電子郵件地址格式。

5.2 Controller層引數校驗

下圖是controller層校驗流程

Controller層校驗流程

@RestController
public class ValidatorController {
    //測試引數校驗
    @RequestMapping("/testValidator")
    public ResponseEntity<String> testValidator(@Valid @RequestBody User  user, BindingResult bindingResult){

        // 是否存在校驗錯誤
        if (bindingResult.hasErrors()) {
            // 獲取校驗不透過欄位的提示資訊
            String errorMsg = bindingResult.getFieldErrors()
                    .stream()
                    .map(FieldError::getDefaultMessage)
                    .collect(Collectors.joining(", "));

            return ResponseEntity.badRequest().body(errorMsg);
        }
        return ResponseEntity.ok("引數校驗成功");
    }

}

解釋一下上面程式碼:

  • @Validated: 告訴 Spring 需要對 User 物件執行校驗; 這個一定不要忘記加上
  • BindingResult : 該類包含校驗不透過時的異常資訊,校驗不透過時,我們透過這個物件來獲取註解中message="xxx"中的內容

注意:當註解校驗不透過時,直接將異常資訊返回給前端其實並不友好,我們可以將異常包裝一下再丟給前端

5.3 測試校驗結果

這裡我們使用postman工具測試一下引數校驗是否成功

入參正確情況

{
    "name":"小凡",
    "sex":"男",
    "age":18,
    "email":"xiezhr@qq.com"
}

入參正確

入參不正確的情況

{
    "name":null,
    "sex":"",
    "age":17,
    "email":"xiezhrqq.com"
}

入參不正確

透過上面的入門小案例,你學會了麼?

上面的返回結果看起來可能不是那麼優雅,那麼怎麼封裝統一返回結果呢,

傳送門在此優雅的封裝返回結果

六、單個引數校驗

上面快速入門中我們說了實體引數校驗,這小節,我們來看看單個引數的校驗

6.1 controller層校驗程式碼

@RequestMapping("/testSingleParmaValidator")
public ResponseEntity<String> testSingleParmaValidator(@NotBlank(message = "姓名不能為空") String name,
                                                       @Min(value = 18,message = "年齡不能小於18")
                                                       @Max(value = 120,message = "年齡不能大於120") Integer age

                                                      ){

    // 引數校驗
    return ResponseEntity.ok("引數校驗成功");
}

6.2 全域性異常捕獲

當引數校驗不透過會發生如下異常資訊

異常資訊

這裡我們不能像上面一樣透過BindingResult 來獲取異常資訊,需要新增全域性異常捕獲校驗失敗異常,具體程式碼如下

@RestControllerAdvice
public class GlobalExceptionHandler {
    //處理ValidationException異常
    @ExceptionHandler(ValidationException.class)
    //返回狀態碼為400
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<String> handleValidationExceptions(ValidationException  ex) {
        String  message = "";
        //判斷異常型別
        if(ex instanceof ConstraintViolationException){
            ConstraintViolationException exs = (ConstraintViolationException) ex;
            //獲取驗證不透過的資訊
            Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
            //遍歷驗證不透過的資訊
            for (ConstraintViolation<?> item : violations) {
                //將驗證不透過的資訊拼接到message中
                message+=item.getMessage()+",";
            }
        }
        //返回錯誤資訊
        return ResponseEntity.badRequest().body(message);
    }

}

6.3 測試校驗結果

入參正確情況

http://localhost:8080/testSingleParmaValidator?name=小凡&age=18

入參正確的情況

入參不正確情況

http://localhost:8080/testSingleParmaValidator?name=&age=17

入參不正確情況

八、引數校驗分組

在實際開發中,我們會遇到這樣的情況:同一個實體類可能會在多個介面中使用,但每次的校驗場景又不一樣。

例如:新增使用者和修改使用者介面,引數都是User 實體,在新增使用者的時候ID欄位 可以為空,但name欄位 不能為空

在修改使用者的是由ID欄位不能為空,這種時候就可以使用引數分組來實現。

8.1 定義驗證分組介面

定義兩個分組介面CreateUserGroup(使用者建立組),UpdateUserGroup(使用者更新組),

分別繼承javax.validation.groups.Default,標識不同的業務場景

public interface CreateUserGroup extends Default {
}

public interface UpdateUserGroup extends Default {
}

注:繼承Default並不是必須的。只是說,如果繼承了Default,那麼@Validated(value = Create.class)的校驗範疇就
為【Create】和【Default】;如果沒繼承Default,那麼@Validated(value = Create.class)的校驗範疇只
為【Create】,而@Validated(value = {Create.class, Default.class})的校驗範疇才為【Create】和【Default】

8.2 分組校驗的使用

① 在實體中新增groups 屬性

@Data
public class User {

    //使用者ID
    @NotNull(message = "使用者ID不能為空",groups = UpdateUserGroup.class)  //使用者更新介面必須傳遞使用者ID
    private Integer id;

    //姓名
    @NotBlank(message = "使用者名稱不能為空",groups = CreateUserGroup.class)  //使用者建立介面必須傳遞使用者名稱
    private  String name;

    //性別
    @NotBlank(message = "性別不能為空")   //註解確保性別不為空
    private String sex;

    //年齡
    @NotNull(message = "年齡不能為空")  //註解確保年齡不為空
    @Max(value = 120,message = "年齡不能大於120")  //註解確保年齡必須小於等於120
    @Min(value = 18,message = "年齡不能小於18")   //註解確保年齡必須大於等於18
    private  Integer age;

    //郵箱
    @Email(message = "郵箱格式不正確")    //註解確保郵箱格式正確
    @NotBlank(message = "郵箱不能為空")
    private String email;
}

②在介面中使用分組

使用 @Validated 註解,並指定要執行的驗證組。

//新增使用者
@PostMapping("/addUser")
public ResponseEntity<User> addUser(@Validated(value= CreateUserGroup.class) @RequestBody User user){

    return ResponseEntity.ok(user);
}
//更新使用者
@PutMapping("/updateUser")
public ResponseEntity<User> updateUserUser(@Validated(value= UpdateUserGroup.class) @RequestBody User user){

    return ResponseEntity.ok(user);
}

我們指定create介面指定CreateUserGroup分組,update介面指定UpdateUserGroup

8.3 測試一下介面

介面入參

{
    "name":"小凡",
    "sex":"男",
    "age":18,
    "email":"xiezhr@qq.com"
}

addUser介面新增使用者,不需要id,驗證透過

新增使用者

updateUser介面修改使用者,需要傳入id,校驗不透過

修改使用者

九、巢狀物件校驗

9.1 構造一個員工資訊表

@Data
public class Emp {
    @NotBlank(message = "員工編號不能為空")
    private  String empNo;
    @NotBlank(message = "員工姓名不能為空")
    private  String empName;
    @NotBlank(message = "員工職位不能為空")
    private  String job;
    @Valid                  //這裡必須使用@Valid註解
    private  Dept dept;
}

@Data
public class Dept {
    @NotBlank(message = "部門編號不能為空")
    private String  deptNo;
    @NotBlank(message = "部門名稱不能為空")
    private String  deptName;
}

在這個示例中, Dept 類包含三個欄位需要校驗: deptNo 和``deptName欄位,透過在 Dept類中的每個欄位上新增相應的校驗註解,然後在Emp類中的dept欄位上新增@Valid` 註解,可以實現對巢狀物件中多個欄位進行引數校驗。

9.2 巢狀物件的使用

@PostMapping("/emp")
public ResponseEntity<String> createOrder(@Valid @RequestBody Emp emp) {
    return ResponseEntity.ok("引數校驗成功");
}

9.3 測試一下

① 正確入參情況

{
    "empNo":"10001",
    "empName":"小凡",
    "job":"程式設計師",
    "dept":{
        "deptNo":"20001",
        "deptName":"研發部111"
    }
}

正確入參情況

② 不正確入參情況

{
    "empNo":"10001",
    "empName":"",
    "job":"程式設計師",
    "dept":{
        "deptNo":"20001",
        "deptName":""
    }
}

不正確入參情況

十、自定義引數校驗

SpringBoot 提供的註解校驗功能可以滿足大多數的驗證需求,但如果在系統中需要實現一些特殊的校驗功能時,

我們可以根據規則自定義校驗

下面我們來手把手教你自定義一個字串校驗,校驗字串必須為大寫或小寫

10.1 自定義註解類

我們要自定義驗證功能,需要首先自定義註解,以便我們在實體類中使用它,程式碼如下

①定義一個列舉類 CaseMode

public enum CaseMode {
    UPPER,
    LOWER;
}

②建立一個自定義的校驗註解 @CheckCase

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
public @interface CheckCase {
    String message() default "字串必須是大寫或小寫";

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

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

    CaseMode value();
}

10.2 自定義驗證業務邏輯類

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 value, ConstraintValidatorContext context) {
        // 如果值為空,則返回true
        if (value == null) {
            return true;
        }

        // 根據caseMode的值,判斷value是否需要轉換大小寫
        if (caseMode == CaseMode.UPPER) {
            return value.equals(value.toUpperCase());
        } else {
            return value.equals(value.toLowerCase());
        }
    }
}

10.3 自定義校驗註解使用

①在Car實體類上新增註解

@Data
public class Car {
    //車牌號
    @CheckCase(value = CaseMode.UPPER,message = "車牌號必須為大寫")
    private  String brand;
    //顏色
    @CheckCase(value = CaseMode.LOWER,message = "顏色必須為小寫")
    private  String color;
}

②在controller 中校驗引數

@GetMapping ("/car")
public ResponseEntity<String> validatorCar(@Valid @RequestBody Car car) {
    return ResponseEntity.ok("引數校驗成功");
}

10.4 測試一下

①入參正確情況

{
    "brand":"雲A.888888",
    "color":"red"
}

入參正確情況

②入參錯誤情況

{
    "brand":"雲a.888888",
    "color":"RED"
}

入參錯誤情況

以上就是本期的全部內容,希望對你有所幫助,我們下期再見 (●'◡'●)

相關文章