1. 程式人生 > 其它 >淺析Spring自定義註解+aop實現對實體類的欄位進行校驗

淺析Spring自定義註解+aop實現對實體類的欄位進行校驗

  API開發中經常會遇到一些對請求資料進行驗證的情況,這時候如果使用註解就有兩個好處:

1、一是驗證邏輯和業務邏輯分離,程式碼清晰

2、二是驗證邏輯可以輕鬆複用,只需要在要驗證的地方加上註解就可以

  因此,我們在業務開發過程中經常遇到形形色色的註解(Java提供了一些基本的驗證註解,比如 @NotNull@Size),框架自有的註解並不是總能滿足複雜的業務需求,我們可以自定義註解來滿足我們的需求。方法很簡單,僅需要兩個東西:

  • 一個自定義的註解,並且指定驗證器
  • 一個驗證器的實現

  欄位註解一般是用於校驗欄位是否滿足要求,比如JSR303校驗依賴就提供了很多校驗註解 ,如@NotNull、@Range等,但是這些註解並不是能夠滿足所有業務場景的。比如我們希望傳入的引數在指定的String集合中,那麼已有的註解就不能滿足需求了,需要自己實現。

1、定義一個@Check註解,通過@interface宣告一個註解

// 校驗物件裡的age域的值是奇數,這時候就可以建立以下註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AgeValidator.class)
public @interface Odd {
    String message() default "Age Must Be Odd";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default
{}; }

  其中:@Constraint 通過使用validatedBy來指定與註解關聯的驗證器

  • @Target指明這個註解要作用在什麼地方,可以是物件、域、構造器等,因為要作用在age域上,因此這裡選擇 FIELD
  • @Retention指明瞭註解的生命週期,可以有SOURCE(僅儲存在原始碼中,會被編譯器丟棄),CLASS(在class檔案中可用,會被VM丟棄)以及RUNTIME(在執行期也被保留),這裡選擇了生命週期最長的RUNTIME
  • @Constraint是最關鍵的,它表示這個註解是一個驗證註解,並且指定了一個實現驗證邏輯的驗證器
  • message()指明瞭驗證失敗後返回的訊息,此方法為@Constraint
    要求
  • groups()payload()也為@Constraint要求,可預設為空,詳細用途可以檢視@Constraint文件

2、建立驗證器:驗證器類需要實現ConstraintValidator泛型介面

  有了註解之後,就需要一個驗證器來實現驗證邏輯:這個就是上面 @Constraint 通過使用validatedBy 指定的類

public class AgeValidator implements ConstraintValidator<Odd,Integer> {
    @Override
    public void initialize(Odd constraintAnnotation) {
    }
    @Override
    public boolean isValid(Integer age, ConstraintValidatorContext constraintValidatorContext) {
        return age % 2 != 0;
    }
}
  • 驗證器有兩個型別引數,第一個是所屬的註解(名稱Odd),第二個是註解作用地方的型別,這裡因為作用在age上,因此這裡用了Integer
  • initialize()可以在驗證開始前呼叫註解裡的方法,從而獲取到一些註解裡的引數
  • isValid()就是判斷是否合法的地方

3、使用方式:定義一個實體類

@Data
public class User {   // 性別只能從 man or women 裡取   @Check(paramValues = {"man", "women"}   private String sex;
  // 年齡只能為奇數
  @Odd(message = "年齡不對")
  private Integer age;
}

  也可以自定義message,就像上面 age 那樣。

  另外需要注意的是:在需要啟用驗證的地方一定要加上@Valid註解,這時候如果請求裡的Student年齡不是奇數,就會得到一個400響應:

@RestController
public class StudentResource {
    @PostMapping("/student")
    public String addStudent(@Valid @RequestBody User user) {
        return "Student Created";
    }
}
{
    "timestamp": "2018-08-15T17:01:44.598+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Odd.student.age",
                "Odd.age",
                "Odd.int",
                "Odd"
            ],
            "arguments": [
                {
                    "codes": [
                        "student.age",
                        "age"
                    ],
                    "arguments": null,
                    "defaultMessage": "age",
                    "code": "age"
                }
            ],
            "defaultMessage": "Age Must Be Odd",
            "objectName": "student",
            "field": "age",
            "rejectedValue": 12,
            "bindingFailure": false,
            "code": "Odd"
        }
    ],
    "message": "Validation failed for object='student'. Error count: 1",
    "path": "/student"
}

  也可以手動來處理錯誤,加上一個BindingResult來接收驗證結果即可:

@RestController
public class StudentResource {
    @PostMapping("/student")
    public String addStudent(@Valid @RequestBody Student student, BindingResult validateResult) {
        if (validateResult.hasErrors()) {
            return validateResult.getAllErrors().get(0).getDefaultMessage();
        }
        return "Student Created";
    }
}

  這樣我們就可以使用自定義引數校驗來補充JSR303預設校驗不夠的情況咯,挺好用的。