1. 程式人生 > 實用技巧 >利用 Bean Validation 來簡化介面請求引數校驗

利用 Bean Validation 來簡化介面請求引數校驗

團隊新來了個校招實習生靜靜,相互交流後發現竟然是我母校同實驗室的小學妹,小學妹很熱情地認下了我這個失散多年的大溼哥,後來...

小學妹:大溼哥,咱們專案裡的 Controller 怎麼都看不到引數校驗處理的程式碼呀?但是程式執行起來,看到有是有校驗的?

大溼哥:哦哦,靜靜,你看到 Controller 類和方法上的 @Validated,還有其他引數的 @NotBlank@Size 這些註解了嗎?

小學妹:看到了,你的意思是這些註解跟引數校驗的處理有關係?

大溼哥:對呀!是不是覺得咱們專案上 Controller 的程式碼特清爽。

小學妹:嗯嗯,很乾淨,完全沒有我在學校寫的專案那一大坨校驗的程式碼。大溼哥能給我講講是怎麼一回事嗎?

大溼哥:好吧!這裡是利用了 Bean Validation 的技巧,下面我來詳細講講。


API 是每個 Web 專案中必不可少的部分,後端開發人員除了要處理大量的 CRUD 邏輯之外,介面的引數校驗與響應格式的規範處理也都佔用了大量的精力。

在接下來的幾篇文章中,我們將介紹 API 編寫的實戰技巧,讓從請求到響應的介面編寫更加優雅、高效。

這篇我們來討論介面請求引數校驗

介面常規校驗案例

我們來定義一個使用者物件 - UserDTO,包含使用者名稱、密碼、性別及地址。地址物件 - AddressDTO,包含省份、城市、詳細地址。

使用者物件的欄位有如下約束:

  • 使用者名稱:
    1. 使用者賬號不能為空
    2. 賬號長度必須是6-11個字元
  • 密碼:
    1. 密碼長度必須是6-16個字元
  • 性別:
    1. 性別只能為 0:未知,1:男,2:女
  • 地址:
    1. 地址資訊不能為空

UserDTO

public class UserDTO {
    /**
     * 校驗規則:
     *
     * 1. 使用者賬號不能為空
     * 2. 賬號長度必須是6-11個字元
     */
    private String name;

    /**
     * 校驗規則:
     * 1. 密碼長度必須是6-16個字元
     */
    private String password;

    /**
     * 校驗規則:
     *
     * 1. 性別只能為 0:未知,1:男,2:女"
     */
    private int sex;

    /**
     * 校驗規則:
     *
     * 1. 地址資訊不能為空
     */
    private AddressDTO address;
  
    // 省略 Getter/Setter
}  

AddressDTO

public class AddressDTO {

    private String province;

    private String city;

    private String detail;
  
    // 省略 Getter/Setter
}  

UserController

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("/create")
    public UserDTO create(@RequestBody UserDTO userDTO) {

        if (userDTO == null) {
            // 此為示例程式碼,正式專案中一般不使用 System.out.println 列印日誌
            System.out.println("使用者資訊不能為空");
            return null;
        }

        // 校驗使用者賬戶
        String name = userDTO.getName();
        if (name == null || name.trim() == "") {
            System.out.println("使用者賬號不能為空");
            return null;
        } else {
            int len = name.trim().length();
            if (len < 6 || len > 11) {
                System.out.println("密碼長度必須是6-11個字元");
                return null;
            }
        }

        // 校驗密碼,抽出一個方法,與校驗使用者賬戶的程式碼做比較
        if (validatePassword(userDTO) == null) {
            return null;
        }

        // 校驗性別
        int sex = userDTO.getSex();
        if (sex < 0 || sex > 2) {
            System.out.println("性別只能為 0:未知,1:男,2:女");
            return null;
        }

        // 校驗地址
        validateAddress(userDTO.getAddress());

        // 校驗完成後,請求的使用者資訊有效,開始處理使用者插入等邏輯,操作成功以後響應。
        return userDTO;
    }

    // 校驗地址,通過丟擲異常的方式來處理
    private void validateAddress(AddressDTO addressDTO) {
        if (addressDTO == null) {
            // 也可以通過丟擲異常來處理
            throw new RuntimeException("地址資訊不能為空");
        }

        validateAddressField(addressDTO.getProvince(), "所在省份不能為空");

        validateAddressField(addressDTO.getCity(), "所在城市不能為空");

        validateAddressField(addressDTO.getDetail(), "詳細地址不能為空");
    }

    // 校驗地址中的每個欄位,並返回對應的資訊
    private void validateAddressField(String field, String msg) {
        if (field == null || field.equals("")) {
            throw new RuntimeException(msg);
        }
    }

    // 將校驗密碼的操作抽取到一個方法中
    private UserDTO validatePassword(@RequestBody UserDTO userDTO) {
        String password = userDTO.getPassword();

        if (password == null || password.trim() == "") {
            System.out.println("使用者密碼不能為空");
            return null;
        } else {
            int len = password.trim().length();
            if (len < 6 || len > 16) {
                System.out.println("賬號長度必須是6-16個字元");
                return null;
            }
        }

        return userDTO;
    }
}

在 UserController 中,我們定義了建立使用者的介面 /users/create。在正式開始業務邏輯處理之前,為了保證接收到的引數有效,我們根據規則編寫了大量的校驗程式碼,即使我們可以採取抽取方法等重構手段進行復用,但依然需要對校驗規則勞心勞力。

那有沒有什麼技巧,能夠避免編寫這大量的引數校驗程式碼呢?

Bean Validation 規範與其實現

上面問題的答案當然是:

實際上,Java 早在 2009 年就提出了 Bean Validation 規範,該規範定義的是一個執行時的資料驗證框架,在驗證之後驗證的錯誤資訊會被馬上返回。並且已經歷經 JSR303、JSR349、JSR380 三次標準的制定,發展到了 2.0。

JSR 規範提案只是提供了規範,並沒有提供具體的實現。具體實現框架有預設的 javax.validation.api,以及 hibernate-validator。目前絕大多使用 hibernate-validator。

javax.validation.api

Java 在 2009 年的 JAVAEE 6 中釋出了 JSR303 以及 javax 下的 validation 包內容。這項工作的主要目標是為 java 應用程式開發人員提供 基於 java 物件的 約束(constraints)宣告和對約束的驗證工具(validator),以及約束元資料儲存庫和查詢 API,以及預設實現。
Java8 開始,Java EE 改名為 Jakarta EE,注意 javax.validation 相關的包移動到了 jakarta.validation 的包下。所以大家看不同的版本的時候,會發現以前的版本包在 javax.validation 包下,Java 8之後在 jakarta.validation。

hibernate-validator

hibernate-validator 框架是另外一個針對 Bean Validation 規範的實現,它提供了 JSR 380 規範中所有內建 constraint 的實現,除此之外還有一些附加的 constraint。

使用 validator 進行請求引數校驗實戰

那 Spring Boot 專案中,Bean Validation 的實現框架怎麼優雅地解決請求引數校驗問題呢?

接下來,我們開始實戰。我們將繼續採用「介面常規校驗案例」章節中的 UserDTO、AddressDTO,欄位的約束一樣。

新建 Spring Boot 專案,引入 spring-boot-start-web 依賴,Spring Boot 2.3.0 之後版本還需要引入 hibernate-validator,之前的版本已經包含 。

校驗 @RequestBody 註解的引數

要校驗使用 @RequestBody 註解的引數,需要 2 個步驟:

  1. 對引數物件的欄位使用約束註解進行標註
  2. 在介面中對要校驗的引數物件標註 @Valid 或者 @Validated

使用約束註解對欄位進行標註

UserDTO

public class UserDTO {

    @NotBlank(message = "使用者賬號不能為空")
    @Size(min = 6, max = 11, message = "賬號長度必須是6-11個字元")
    private String name;

    @Size(min = 6, max = 16, message = "密碼長度必須是6-16個字元")
    private String password;

    @Range(min = 0, max = 2, message = "性別只能為 0:未知,1:男,2:女")
    private int sex;

    @NotNull(message = "地址資訊不能為空")
    @Valid
    private AddressDTO address;
  
    // 省略 Getter/Setter
}  

AddressDTO

public class AddressDTO {

    @NotBlank(message = "所在省份不能為空")
    private String province;

    @NotBlank(message = "所在城市不能為空")
    private String city;

    @NotBlank(message = "詳細地址不能為空")
    private String detail;

    // 省略 Getter/Setter
} 

可以看到,我們在需要校驗的欄位上使用 @NotNull@Size@Range 等約束註解進行了標註,在 AddressDTO 上還使用了 @Valid,並且註解中定義了 message 等資訊。

為物件型別引數新增 @Validated

現在再來看 UserController 中新建使用者介面的處理(注意:這裡是通過 Content-Type: application/json 提交的,請求引數需要在 @RequestBody 中獲取)。我們在需要校驗的 UserDTO 引數前新增 @Validated 註解。省略掉新增使用者的邏輯之後,沒有其他的顯式校驗的程式碼。

/**
  * 建立使用者,通過 Content-Type: application/json 提交
  *
  * Validator 校驗失敗將丟擲 {@link MethodArgumentNotValidException}
  *
  * @param userDTO
  * @return
  */
@PostMapping("/create-in-request-body")
public UserDTO createInRequestBody(@Validated @RequestBody UserDTO userDTO) {
    // 通過 Validator 校驗引數,開始處理使用者插入等邏輯,操作成功以後響應
    return userDTO;
}

驗證校驗結果

啟動 Spring Boot 應用,用 Postman 呼叫請求,觀察結果。

輸入不符合要求的欄位後,伺服器返回了錯誤的結果。(經試驗:Spring Boot 2.1.4.RELEASE 版本和 Spring Boot 2.3.3.RELEASE 版本輸出結果不一樣,前者輸出還包含 errors 展示具體每個不符合校驗規則的明細。)

在 Idea 的 Console 中可以看到如下日誌,這說明不符合校驗規則的引數已經被驗證。

Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public io.ron.demo.validator.use.dto.UserDTO io.ron.demo.validator.use.controller.UserController.createInRequestBody(io.ron.demo.validator.use.dto.UserDTO) with 3 errors: [Field error in object 'userDTO' on field 'password': rejected value [123]; codes [Size.userDTO.password,Size.password,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.password,password]; arguments []; default message [password],16,6]; default message [密碼長度必須是6-16個字元]] [Field error in object 'userDTO' on field 'name': rejected value [lang1]; codes [Size.userDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.name,name]; arguments []; default message [name],11,6]; default message [賬號長度必須是6-11個字元]] [Field error in object 'userDTO' on field 'sex': rejected value [3]; codes [Range.userDTO.sex,Range.sex,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.sex,sex]; arguments []; default message [sex],2,0]; default message [性別只能為 0:未知,1:男,2:女]] ]

如果請求引數都滿足條件,則能正確響應結果。

校驗不使用 @RequestBody 註解的物件

有時我們也會編寫這樣的介面,介面方法中的物件型別引數不使用 @RequestBody 註解。使用 @RequestBody 註解的物件型別引數需要明確以 Content-Type: application/json 上傳。而這種寫法可以以 Content-Type: application/x-www-form-urlencoded 上傳。

這樣編寫介面,校驗方法與被 @RequestBody 註解的物件引數一樣。

驗證校驗結果

在 Idea 的 Console 中可以看到如下日誌:

2020-09-18 17:56:05.191  WARN 9734 --- [nio-9001-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'userDTO' on field 'name': rejected value [12345]; codes [Size.userDTO.name,Size.name,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.name,name]; arguments []; default message [name],11,6]; default message [賬號長度必須是6-11個字元]
Field error in object 'userDTO' on field 'address.detail': rejected value [      ]; codes [NotBlank.userDTO.address.detail,NotBlank.address.detail,NotBlank.detail,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.address.detail,address.detail]; arguments []; default message [address.detail]]; default message [詳細地址不能為空]]

請注意日誌中的異常型別與 @RequestBody 註解的校驗異常的區別。

這裡報的異常型別是:org.springframework.validation.BindException

而上一節中報的異常型別是:org.springframework.web.bind.MethodArgumentNotValidException

在下一節中的情形,報的異常則是:javax.validation.ConstraintViolationException

異常的處理我們將在下一篇文章中說明。請關注我的公眾號:精進Java(ID:craft4j),第一時間獲取知識動態。

校驗 @PathVariable@RequestParam 註解的引數

在真實專案中,不是所有的介面都接受物件型別的引數,如分頁介面中的頁碼會使用 @RequestParam 註解;Restful 風格的介面會通過 @PathVariable 來獲取資源 ID 等。這些引數無法通過上面的方法被 validator 校驗。

要校驗 @PathVariable@RequestParam 註解的引數,需要 2 個步驟:

  1. 在要校驗的介面類上標註 @Validated 註解
  2. 在簡單型別引數前標註 @PathVariable@RequestParam
/**
 * 測試 @PathVariable 引數的校驗
 *
 * Validator 校驗失敗將丟擲 {@link ConstraintViolationException}
 *
 * @param id
 * @return
 */
@GetMapping("/user/{id}")
public UserDTO retrieve(@PathVariable("id") @Min(value = 10, message = "id 必須大於 10") Long id) {
    return buildUserDTO("lfy", "qwerty", 1);
}

/**
 * 測試 @RequestParam 引數校驗
 *
 * 在方法上加 @Validated 無法校驗 @RequestParam 與 @PathVariable
 *
 * 必須在類上 @Validated
 *
 * Validator 校驗失敗將丟擲 {@link ConstraintViolationException}
 *
 * @param name
 * @param password
 * @param sex
 * @return
 */
// @Validated
@GetMapping("/validate")
public UserDTO validate(@NotNull @Size(min = 6, max = 11, message = "賬號長度必須是6-11個字元") @RequestParam("name") String name,
                        @RequestParam("password") @Size(min = 6, max = 16, message = "密碼長度必須是6-16個字元") String password,
                        @RequestParam("sex") @Range(min = 0, max = 2, message = "性別只能為 0:未知,1:男,2:女") int sex) {
    return buildUserDTO(name, password, sex);
}

private UserDTO buildUserDTO(String name, String password, int sex) {
    UserDTO userDTO = new UserDTO();
    userDTO.setName(name);
    userDTO.setPassword(password);
    userDTO.setSex(sex);
    return userDTO;
}

驗證 @PathVariable 校驗結果

輸入小於 10 的 id,結果如下:

Idea 中 Console 報錯誤日誌如下:

2020-09-18 17:23:55.744 ERROR 9734 --- [nio-9001-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: retrieve.id: id 必須大於 10] with root cause

javax.validation.ConstraintViolationException: retrieve.id: id 必須大於 10
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE]
  ......

驗證 @RequestParam 校驗結果

輸入不滿足要求的引數 name 和 password

Idea 中 Console 報錯誤日誌如下:

2020-09-18 17:37:51.875 ERROR 9734 --- [nio-9001-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: validate.password: 密碼長度必須是6-16個字元, validate.name: 賬號長度必須是6-11個字元] with root cause

javax.validation.ConstraintViolationException: validate.password: 密碼長度必須是6-16個字元, validate.name: 賬號長度必須是6-11個字元
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	......

分組校驗

還有這樣的場景,如在新建使用者的介面中,使用者的 ID 欄位為空;而在更新使用者的介面中,使用者的 ID 欄位則要求是必填。針對同一個使用者實體物件,我們可以利用 validator 提供的分組校驗。

分組校驗的步驟如下:

  1. 定義分組介面。
  2. 對欄位使用約束註解進行標註,使用 groups 引數進行分組。
  3. @Validated 標識引數,並設定 groups 引數。

普通分組

Update:我們定義一個 Update 分組介面

public interface Update {

}

UserDTO:對欄位上的約束註解新增 groups 引數。下面對 id 的 @NotNull 添加了 Update 分組,對 name 欄位的 @NotBlank 添加了 Update 分組及 validator 的預設 Default 分組。

@NotNull(message = "使用者ID不能為空", groups = { Update.class })
private Long id;

@NotBlank(message = "使用者賬號不能為空", groups = { Update.class, Default.class })
@Size(min = 6, max = 11, message = "賬號長度必須是6-11個字元")
private String name;

UserController:在更新使用者介面中對引數使用 @Validated 註解並設定 Update 分組。

@PutMapping("/update")
public UserDTO update(@Validated({Update.class}) @RequestBoy UserDTO userDTO) {
    // 通過 Validator 校驗引數,開始處理使用者插入等邏輯,操作成功以後響應
    return userDTO;
}

這裡將只會對設定了分組為 Update 的約束進行校驗,當 id 為空或者 name 為空或者空白的時候會報約束錯誤。當 id 與 name 均不為空時,即使 name 的長度不在 6-11 個字元之間,也不會校驗。

組序列

除了按組指定是否驗證之外,還可以指定組的驗證順序,前面組驗證不通過的,後面組將不進行驗證。

OrderedGroup:定義了校驗組的順序,Update 優先於 Default

@GroupSequence({ Update.class, Default.class })
public interface OrderedGroup {

}

UserController:在介面引數中 @Validated 註解設定引數 OrderedGroup.class

@PostMapping("/ordered")
public UserDTO ordered(@Validated({OrderedGroup.class}) @RequestBody UserDTO userDTO) {
    // 通過 Validator 校驗引數,開始處理使用者插入等邏輯,操作成功以後響應
    return userDTO;
}

與普通分組中的案例結果不同,這裡會優先校驗 id 是否為空或者 name 是否為空或者空白,即 Update 分組的約束;Update 分組約束滿足之後,還會進行其他引數的校驗,因為其他引數都預設為 Default 分組。

hibernate-validator 的校驗模式

從上面的案例中,細心的你可能已經發現了這樣的現象:所有的引數都做了校驗。實際上只要有一個引數校驗不通過,我們就可以響應給使用者。而 hibernate-validator 可以支援兩種校驗模式:

  1. 普通模式,預設是這種模式,該模式會校驗完所有的屬性,然後返回所有的驗證失敗資訊
  2. 快速失敗返回模式,這種模式下只要有一個引數校驗失敗就立即返回

開啟快速失敗返回模式

@Configuration
public class ValidatorConfig {

    @Value("${hibernate.validator.fail_fast:false}")
    private boolean failfast;

    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // failFast 為 true 時,只要出現校驗失敗的情況,就立即結束校驗,不再進行後續的校驗。
                .failFast(failfast)
                .buildValidatorFactory();

        return validatorFactory.getValidator();
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        // 設定 validator 模式為快速失敗返回
        postProcessor.setValidator(validator());
        return postProcessor;
    }
}

我們需要對 MethodValidationPostProcessor 設定開啟快失敗返回模式的 validator。而 validator 則只需設定 hibernate.validator.fail_fast 屬性為 true。

再次執行 Spring Boot 專案,進行測試,我們會發現現在只要有一個引數校驗失敗,就立即返回了。

自定義約束實現

vaidation-api 與 hibernate-validator 提供的約束註解已經能夠滿足我們絕大多數的引數校驗要求,但有時我們可能也需要使用自定義的 Validator 校驗器。

自定義約束實現與使用包含如下步驟:

  1. 自定義約束註解
  2. 實現 ConstraintValidator 來自定義校驗邏輯

通用列舉型別約束

我們以自定義一個相對通用的列舉型別約束來演示。

自定義 @EnumValue 列舉指約束註解

enumClass 標識欄位取值對應哪個列舉型別,enumMethod 則是需要列舉類定義一個用於驗證取值是否有效的驗證方法,如果為空的話,我們會預設提供處理引數為整型與字串型的情況。需要在註解上使用 @Constraint(validatedBy) 來設定具體使用的校驗器。

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValue.EnumValidator.class)
public @interface EnumValue {

    String message() default "無效的列舉值";

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

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

    Class<? extends Enum<?>> enumClass();

    String enumMethod() default "";
}

編寫 EnumValidator 來自定義校驗邏輯

class EnumValidator implements ConstraintValidator<EnumValue, Object> {

    private Class<? extends Enum<?>> enumClass;

    private String enumMethod;

    @Override
    public void initialize(EnumValue enumValue) {
        enumMethod = enumValue.enumMethod();
        enumClass = enumValue.enumClass();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        if (value == null) {
            return Boolean.TRUE;
        }

        if (enumClass == null) {
            return Boolean.TRUE;
        }

        Class<?> valueClass = value.getClass();
        if (enumMethod == null || enumMethod.equals("")) {
            String valueClassName = valueClass.getCanonicalName();
            // 處理引數可以轉為列舉值 ordinal 的情況
            if (valueClassName.equals("java.lang.Integer")) {
                return enumClass.getEnumConstants().length > (Integer) value;
            }
            // 處理引數為列舉名稱的情況
            else if (valueClassName.equals("java.lang.String")) {
                return Arrays.stream(enumClass.getEnumConstants()).anyMatch(e -> e.toString().equals(value));
            }
            throw new RuntimeException(String.format("A static method to valid enum value is needed in the %s class", enumClass));
        }

        // 列舉類自定義取值校驗
        try {
            Method method = enumClass.getMethod(enumMethod, valueClass);
            if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
                throw new RuntimeException(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass));
            }

            if (!Modifier.isStatic(method.getModifiers())) {
                throw new RuntimeException(String.format("%s method is not static method in the %s class", enumMethod, enumClass));
            }

            Boolean result = (Boolean) method.invoke(null, value);
            return result == null ? false : result;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e);
        }
    }
}

實現 ConstraintValidator 的兩個方法 initializeisValid,一個是初始化引數的方法,另一個就是校驗邏輯的方法。

在校驗方法中,當約束註解沒有定義 enumMethod 時,我們根據傳入需要校驗的引數提供整型與字元型的兩種預設校驗,可以仔細看原始碼第 23-34 行。從 37-54 行的程式碼可以看到,除開上面的 2 種情況下,列舉型別需要提供一個自定義的校驗方法。

在專案中使用

Gender:我們定義一個表示性別的列舉。這裡我們編寫了一個判斷列舉取值是否有效的靜態方法 isValid()

public enum Gender {

    UNKNOWN,

    MALE,

    FEMALE;

    /**
     * 判斷取值是否有效
     *
     * @param val
     * @return
     */
    public static boolean isValid(Integer val) {
        return Gender.values().length > val;
    }
}

UserDTO:給 sex 欄位的設定 @EnumValue 約束

@Range(min = 0, max = 2, message = "性別只能為 0:未知,1:男,2:女")
@EnumValue(enumClass = Gender.class)
private int sex;

接下來就是執行程式,發請求觀察校驗結果了。

總結

前面我們從一個常規校驗案例開始,說明了 Bean Validation 規範及其實現,並從實戰角度出發介紹了各種場景下的校驗,包括:

  1. 使用 @RequestBody 和不使用 @RequestBody 註解的物件型別引數
  2. 使用 @RequestParam@PathVariable 註解的簡單型別引數
  3. 分組校驗
  4. 快速失敗返回校驗模式
  5. 自定義約束校驗

總體而言,使用 validator 能夠極大的方便請求引數的校驗,簡化校驗相關的實現程式碼。但是,細心的讀者也發現了,本文中所有的介面當有引數校驗失敗時,都是報了異常,返回的響應中直接報 400 或者 500 的錯誤。響應不直觀,不規範,給到前端也無法方便高效的處理。

在接下來的文章中,我將繼續為大家帶來全域性異常處理、統一響應結構的知識與實戰。

文中涉及的程式碼已經開源在我的 Github 倉庫 ron-point 中。如果覺得不錯請點個 star,歡迎一起討論和交流。