1. 程式人生 > 其它 >spring-boot-@Valid和@Validated詳解

spring-boot-@Valid和@Validated詳解

----------------------------------------------------------------------------------------

在實際的專案開發中,經常會遇到對引數進行校驗的場景,最常見的就是後端需要對前端傳過來的資料進行校驗。

我理解的資料校驗大致分為兩類:

一類是對資料本身進行校驗,不涉及與資料庫互動的,比如正則校驗、非空校驗、指定的列舉資料、最大值、最小值等等。

二類是資料的校驗需要和資料庫互動的,比如是否唯一(資料庫中是否存在)、數量限制(資料庫中只能允許存在10條資料)等等。

由於第二類其實屬於業務邏輯,這裡不做討論,本文主要是針對第一類場景的資料校驗。

其實也可以在業務程式碼中去做校驗判斷,但是這樣就不夠優雅了不是嗎,話不多說直接開始正文

按如下目錄進行講述(點選可以直接定位到感興趣的章節)

1、@Valid和@Validated介紹以及對應的Maven座標

2、@Valid和@Validated中常用的註解

3、@Valid和@Validated區別和對應使用場景

4、@Valid的巢狀校驗(校驗的物件中引入的其他物件或者List物件的校驗)

5、@Validated的分組校驗(不同的分組不同的校驗策略)

6、@Validated中的分組校驗時@GroupSequence使用(指定欄位的校驗順序)

7、快速失敗機制(單個引數校驗失敗後,不再對剩下的引數進行校驗)

8、自定義校驗註解,實現特殊的校驗邏輯

9、全域性異常處理,統一返回校驗異常資訊

10、@Interface List的使用場景(補充)

11、@Valid和@Validated組合使用(補充)

1、@Valid和@Validated介紹以及對應的Maven座標(回到目錄)

 @Valid和@Validated主要是用於表單校驗

Maven一般是跟隨spring-boot-starter-parent,也可以自行選擇對應的版本,目前spring-boot-starter-validation最新的版本是2.7.0,Maven中心

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

2、@Valid和@Validated中常用的註解(回到目錄)

常用的註解如下圖,可能由於版本不同略有出入,具體的含義可以看註解上的註釋,下面提供了一份整理的註解含義

@AssertFalse 限制必須為false
@AssertTrue 限制必須為true
@DecimalMax(value) 限制必須為一個不大於指定值的數字
@DecimalMin(value) 限制必須為一個不小於指定值的數字
@Digits(integer,fraction) 限制必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
@Email 驗證註解的元素值是Email,也可以通過正則表示式和flag指定自定義的email格式
@Future 限制必須是一個將來的日期
@FutureOrPresent 未來或當前的日期,此處的present概念是相對於使用約束的型別定義的。例如校驗的引數為Year year = Year.now();此時約束是一年,那麼“當前”將表示當前的整個年份。
@Max(value) 限制必須為一個不大於指定值的數字
@Min(value) 限制必須為一個不小於指定值的數字
@Negative 絕對的負數,不能包含零,空元素有效可以校驗通過
@NegativeOrZero 包含負數和零,空元素有效可以校驗通過
@NotBlank 驗證註解的元素值不為空(不為null、去除首位空格後長度為0),不同於@NotEmpty,@NotBlank只應用於字串且在比較時會去除字串的空格
@NotEmpty 驗證註解的元素值不為null且不為空(字串長度不為0、集合大小不為0)
@NotNull 限制必須不為null
@Null 限制只能為null
@Past 限制必須是一個過去的日期
@PastOrPresent 過去或者當前時間,和@FutureOrPresent類似
@Pattern(value) 限制必須符合指定的正則表示式
@Positive 絕對的正數,不能包含零,空元素有效可以校驗通過
@PositiveOrZero 包含正數和零,空元素有效可以校驗通過
@Size(max,min) 限制字元長度必須在min到max之間

3、@Valid和@Validated區別和對應使用場景(回到目錄)

@Valid可以實現巢狀校驗,對於物件中引用了其他的物件,依然可以校驗

@Validated可以對引數校驗進行分組,例如一個物件裡面有一個欄位id,id在新增資料時可以為空,但是在更新資料時不能為空,此時就需要用到校驗分組

具體的使用見下面的章節

為了方便理解和構造使用場景,目前假設存在三個實體物件,分別是ProjectDTO(專案)、TeamDTO(團隊)和MemberDTO(成員),彼此的關係是,一個專案中存在一個團隊,一個團隊中存在多個成員,實體類裡面的屬性虛構,目的是為了舉例校驗的相關注解。

ProjectDTO(專案)實體類:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProjectDTO {

    @NotBlank(message = "ID不能為空", groups = {TestValidGroup.Update.class})
    private String id;

    @NotBlank
    @Pattern(regexp = "[a-zA-Z0-9]", message = "只允許輸入數字和字母")
    private String strValue;

    @Min(value = -99, message = "值不能小於-99")
    @Max(value = 100, message = "值不能超過100")
    private Integer intValue;

    @Negative(message = "值必須為負數")
    private Integer negativeValue;

    @EnumValue(strValues = {"agree", "refuse"})
    private String strEnum;

    @EnumValue(intValues = {1983, 1990, 2022})
    private Integer intEnum;

    @Valid
    private TeamDTO teamDTO;

}

TeamDTO(團隊)實體類:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TeamDTO {

    @FutureOrPresent(message = "只能輸入當前年份或未來的年份")
    private Year nowYear;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @Future(message = "只能是未來的時間")
    private Date futureTime;

    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    @Past(message = "只能是過去的時間")
    private Date pastTime;

    @Email(message = "請輸入正確的郵箱")
    private String email;

    @Valid
    private List<MemberDTO> list;

}

MemberDTO(成員)實體類:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDTO {

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

    @EnumValue(intValues = {0, 1, 2}, message = "性別值非法,0:男,1:女,2:其他")
    private Integer sex;

}

4、@Valid的巢狀校驗(校驗的物件中引入的其他物件或者List物件的校驗)(回到目錄)

userInfo方法則是採用@Valid方式進行校驗,傳入的物件時TeamDTO(團隊),實體類裡面具體的引數可以看第3部分裡面實體類的具體程式碼TeamDTO(團隊)實體類

@RestController
@RequestMapping("/valid")
public class TestValidController {
    
    @PostMapping("/userInfo")
    public BaseResponse userInfo(@Valid @RequestBody TeamDTO teamDTO) {
        return new BaseResponse(teamDTO);
    }
}

關鍵點在於TeamDTO裡面的屬性List<MemberDTO> list,上面加上@Valid註解,如下:

@Valid
private List<MemberDTO> list;

postman測試結果如下:

可以看到list裡面,MemberDTO也被校驗了,name和sex不合法。

5、@Validated的分組校驗(不同的分組不同的校驗策略)(回到目錄)

例如有一個場景,更新專案資訊,專案id是必須要傳的,但是在新增專案時,id可以不傳,新增和更新用的同一個實體物件,這個時候需要根據不同的分組區分,不同的分組採用不同的校驗策略,具體查詢ProjectDTO(專案)實體類

@NotBlank(message = "ID不能為空", groups = {TestValidGroup.Update.class})
private String id;

如上,註解引數中存在一個groups,表示將該引數歸為update組,可以指定一個引數屬於多個組

Controller的程式碼如下,@Validated有一個引數值value,可以校驗指定分組的屬性,下面就是指定校驗groups包含TestValidGroup.Update.class的屬性,在ProjectDTO中只有id這個屬性的groups滿足條件,所以只會校驗id這個引數。

@RestController
@RequestMapping("/valid")
public class TestValidController {

    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);

    }
}

group如何自定義,其實很簡單,就是自己定義一個介面,這個介面的作用只是用來分組,自己建立一個介面,程式碼如下:

分別表示在新增和更新兩種情況,可以按實際需求在內部新增多個介面

public interface TestValidGroup {

    interface Insert {

    }

    interface Update {

    }
}

注意:未顯示指定groups的欄位,預設歸於javax.validation.groups包下的Default.class(預設組)

@Validated的value不指定組時,只校驗Default組的欄位

@Validated的value指定組時,只校驗屬於指定組的欄位,屬於Default組的欄位不會被校驗

若想指定組和預設組都被校驗,有兩種方式:

1、在@Validated的value中加入預設組,如下:

@PostMapping("/post")
public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
    return new BaseResponse(testAnnotationDto);
}

2、將指定的Update介面繼承Default介面,如下:

public interface TestValidGroup {

    interface Insert {

    }

    interface Update extends Default {

    }

}

6、@Validated中的分組校驗時@GroupSequence使用(指定欄位的校驗順序)(回到目錄)

從上面的Swagger除錯截圖可以知道,返回的是所有欄位的校驗結果,所以存在一個問題,那就是多個校驗欄位之間的順序如何保證,如果不指定順序,那麼每次校驗的順序就會不同,那個錯誤提示資訊也就不同,一些特殊場景會要求固定錯誤順序,例如自動化測試指令碼,每次都需要將返回的校驗結果和預期結果比較,返回的校驗結果一直變化就會有問題。

Controller層程式碼如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Validated(value = {TestValidGroup.Update.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);
    }
}

指定校驗順序就會用到@GroupSequence註解,這個註解使用在group的介面上,可以針對每一個引數都進行分組,然後通過該註解去指定順序,程式碼如下,例如update時,校驗的順序就是先校驗group屬於Id.class的欄位,再校驗group屬於StrValue的欄位。

public interface TestValidGroup {


    @GroupSequence(value = {StrValue.class})
    interface Insert {

    }

    @GroupSequence(value = {Id.class, StrValue.class})
    interface Update {

    }

    interface Id {

    }

    interface StrValue {

    }
}

注意:此時不是校驗group屬於Update.class的欄位,而是校驗 group屬於@GroupSequence的value中的那些介面(Id.class, StrValue.class) 的欄位,如下:

正確用法:

@NotBlank(message = "ID不能為空", groups = {TestValidGroup.Id.class})
private String id;

錯誤用法:

@NotBlank(message = "ID不能為空", groups = {TestValidGroup.Update.class})
private String id;

小知識:一個欄位上存在多個註解時,例如@Max和@NotBlank,是按註解從上至下的順序進行校驗的。

7、快速失敗機制(單個引數校驗失敗後,立馬丟擲異常,不再對剩下的引數進行校驗)(回到目錄)

實際情況中,有時候並不需要校驗完所有的引數,只要校驗失敗,立馬丟擲異常,Validation提供了快速失敗的機制,程式碼如下:

@Configuration
public class ValidConfig {

    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 快速失敗模式
                .failFast(true)
                .buildValidatorFactory();
        return validatorFactory.getValidator();
    }
}

8、自定義校驗註解,實現特殊的校驗邏輯(回到目錄)

有時候會存在一些特殊的校驗邏輯,已有的註解並不能滿足要求,此時就可以自定義校驗註解,自己實現特殊的校驗邏輯,一般分為兩步,1、自定義一個註解。2、實現該註解的校驗邏輯

例子場景:目前想實現一種校驗,傳入的字串必須在指定的字串陣列中存在,傳入的數字必須在指定的Integer陣列中存在,類似於列舉值。

1、自定義一個註解

自定義註解的方式不用多說,主要講下和校驗相關的地方,@Constraint(validatedBy = {EnumValueValidated.class}),這個註解很關鍵,裡面的validatedBy = {EnumValueValidated.class}是指定具體的校驗類,

具體的校驗邏輯在EnumValueValidated類裡面實現。另外就是註解裡面的一些屬性,例如message、groups、payload和內部的一個@List註解(這個註解的使用場景後面會講到),這裡可以參考validation已有的註解,基本都是很有用的。

然後就是定義自己需要的一些特殊的屬性,方便校驗,例如下面的註解中就包含了,isRequire、strValues、intValues。

@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Constraint(validatedBy = {EnumValueValidated.class})
public @interface EnumValue {

    /**
     * 是否需要(true:不能為空,false:可以為空)
     */
    boolean isRequire() default false;

    /**
     * 字串陣列
     */
    String[] strValues() default {};

    /**
     * int陣列
     */
    int[] intValues() default {};

    /**
     * 列舉類
     */
    Class<?>[] enumClass() default {};

    String message() default "所傳引數不在允許的值範圍內";

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

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

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        EnumValue[] value();
    }
}

2、實現該註解的校驗邏輯

具體的程式碼如下,implements ConstraintValidator<EnumValue, Object>,實現兩個方法,分別是initialize(初始化方法)和isValid(校驗方法),initialize()主要是載入讀取註解上的值並賦值給類變數,

isValid()是實現具體的校驗邏輯,此處不具體說明,可自行實現。

public class EnumValueValidated implements ConstraintValidator<EnumValue, Object> {
    private boolean isRequire;
    private Set<String> strValues;
    private List<Integer> intValues;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        strValues = Sets.newHashSet(constraintAnnotation.strValues());
        intValues = Arrays.stream(constraintAnnotation.intValues()).boxed().collect(Collectors.toList());
        isRequire = constraintAnnotation.isRequire();

        //將列舉類的name轉小寫存入strValues裡面,作為校驗引數
        Optional.ofNullable(constraintAnnotation.enumClass()).ifPresent(e -> Arrays.stream(e).forEach(
                c -> Arrays.stream(c.getEnumConstants()).forEach(v -> strValues.add(v.toString().toLowerCase()))
        ));
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null && !isRequire) {
            return true;
        }

        if (value instanceof String) {
            return strValues.contains(value);
        }
        if (value instanceof Integer) {
            return intValues.stream().anyMatch(e -> e.equals(value));
        }

        return false;
    }
}

9、全域性異常處理,統一返回校驗異常資訊(回到目錄)

專案中一般會針對異常進行統一處理,valid校驗失敗的異常是MethodArgumentNotValidException,所以可以攔截此類異常,進行異常資訊的處理,捕獲後的具體邏輯,自行實現,例子程式碼如下:

@Slf4j
@RestControllerAdvice
public class ExceptionHandlerConfig {
    /**
     * 攔截valid引數校驗返回的異常,並轉化成基本的返回樣式
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public BaseResponse dealMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("this is controller MethodArgumentNotValidException,param valid failed", e);
        List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
        String message = allErrors.stream().map(s -> s.getDefaultMessage()).collect(Collectors.joining(";"));
        return BaseResponse.builder().code("-10").msg(message).build();
    }
}

10、@Interface List的使用場景(補充)(回到目錄)

有時候會出現這種需求,同一個欄位在不同的場景下,需要採用不同的校驗規則,並返回不同的異常資訊,目前有兩種方式,一種是採用@List的方式,一種是在欄位上重複使用同一個註解,具體程式碼如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
public class BaseDTO {

    @NotBlank.List({
            @NotBlank(message = "專案BaseId不能為空", groups = {TestValidGroup.Project.class}),
            @NotBlank(message = "團隊BaseId不能為空", groups = {TestValidGroup.Team.class})
    })
    private String baseId;

    @Max(value = 10, message = "專案BaseId不能大於10", groups = {TestValidGroup.Project.class})
    @Max(value = 30, message = "團隊BaseId不能大於30", groups = {TestValidGroup.Team.class})
    private Integer number;
}

目的是通過指定註解歸屬於不同的分組來到達區分的效果。

Controller程式碼如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/projectList")
    public BaseResponse projectList(@Validated(value = {TestValidGroup.Project.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }

    @PostMapping("/teamList")
    public BaseResponse projectTeam(@Validated(value = {TestValidGroup.Team.class}) @RequestBody BaseDTO baseDTO) {
        return new BaseResponse(baseDTO);
    }
}

11、@Valid和@Validated組合使用(補充)(回到目錄)

@Validated和Valid肯定是可以組合使用的,一種是分組,一種是巢狀,單獨使用的注意點已經在上面的部分寫過,下面簡單描述下在Controller程式碼中的使用,其實很簡單,就是在實體類(ProjectDTO)上同時加上這兩個註解,程式碼如下:

@RestController
@RequestMapping("/valid")
public class TestValidController {
    @PostMapping("/post")
    public BaseResponse testValidPostRequest(@Valid @Validated(value = {TestValidGroup.Update.class, Default.class}) @RequestBody ProjectDTO testAnnotationDto) {
        return new BaseResponse(testAnnotationDto);

    }
}

----------------------------------------------------------------------------------------

@Validated註解

@Validated註解是為了給請求介面時,判斷物件的值是否是你需要的屬性做判斷。

在程式設計中我使用的主要用的有:

1、@NotNull (不能為null)

2、@NotEmpty (不為空也不能為null,其主要限制String型別)

3、@NotBlank (不能為空,去除一開始的空格後長度不能為0)

在網上還看到一些常用註釋,現記錄下來:

感覺比較實用的:

1、@Size(max,min) (限制字元長度必須在min到max之前)

2、@Past (限制必須是過去的日期)

3、@PastOrPresent (限制必須是過去的日期或者是當前時間)

4、@Future (限制必須是以後的日期)

5、@FutureOrPresent (限制必須是以後的日期或者是當前時間)

6、@Max(value) (限制必須為不大於value的值)

7、@Min(value) (限制必須為不小於value的值)

8、@Pattern(value) (限制必須符合指定的正則表示式)

9、@Email (限制必須為email格式)

不常用的:

1、@Null (限制只能為空)

2、@AssertFalse (限制必須是false)

3、@AssertTrue. (限制必須為true)

4、@DecimalMax(value) (限制必須為不大於value的值)

5、@DecimalMin(value) (限制必須為不小於value的值)

6、@Digits(Integer,fraction)(限制必須為一個小數,且整數部分位數不超過Intger,小數不超過fraction)

7、@Negative (限制必須為負整數)

8、@NegativeOrZero (限制必須為負整數或零)

8、@Positive (限制必須為正整數)

9、@PositiveOrZero (限制必須為正整數或零)

現在來講一下如何使用:

1、匯入依賴(有兩種)

第一種:(我使用的是第一種)

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
第二種:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2、建立物件、介面;

建立物件

@Data
public class XxxAddParam{

@NotNull(message = "使用者id不能為空")
private Integer id;

@NotEmpty(message = "使用者姓名不能為空")
private String name;
}
建立介面

@Slf4j
@RestController
@RequestMapping("/xxx")
public class XxxController {

@PostMapping("/addXxx")
public Result addXxx(@RequestBody @Validated XxxAddParam XxxAddParam){
return Result.success();
}

}
在一開始可能會遇到如下問題:
1、@Validated註解的預設異常過長;

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.xwdBoy.common.helper.Result com.xwdBoy.web.controller.XxxController.addXxx(com.xwdBoy.param.XxxAddParam): [Field error in object 'XxxAddParam' on field 'name': rejected value []; codes [NotEmpty.XxxAddParam.name,NotEmpty.name,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [XxxAddParam.name,name]; arguments []; default message [name]]; default message [使用者姓名不能為空]]

2、無法對List<xx>物件進行驗證;

第一個問題是需要加上全域性異常處理就可以通過:
/**
* HTTP介面統一的錯誤處
* @author sj
*/
@ControllerAdvice
@Slf4j
@Priority(1)
public class GlobalExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
List<FieldError> errors = bindingResult.getFieldErrors();
//初始化錯誤資訊大小
Result result = new Result();
for (FieldError error : errors) {
result.setMsg(error.getDefaultMessage());
result.setCode(ResultEnum.ERROR.getCode());
return result;
}
return Result.error(ResultEnum.ERROR.getCode(), ResultEnum.ERROR.getMsg());
}
@ExceptionHandler(BizException.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleBizExceptions(BizException e) {
log.error(e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}

@ResponseBody
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result httpRequestMethodNotSupportedExceptionException(HttpRequestMethodNotSupportedException e) {
String message = Optional.ofNullable(e.getMessage()).orElse(ResultEnum.HTTP_REQUEST_METHOD_NOT_SUPPORTED.getMsg());
log.warn("HttpRequestMethodNotSupportedException:", e);
return Result.error(ResultEnum.HTTP_REQUEST_METHOD_NOT_SUPPORTED.getCode(), message);
}

@ResponseBody
@ExceptionHandler(MultipartException.class)
public Result fileUploadExceptionHandler(MultipartException e) {
log.warn("上傳檔案異常:{}", e.getMessage());
return Result.error(ResultEnum.ERROR.getCode(), "檔案過大,單個不超200M");
}
}

第二個問題對list進行操作(在入參的時候使用ValidList<xx>):

@Data
public class ValidList<E> implements List<E> {

@Valid
private List<E> list = new LinkedList<>();

@Override
public int size() {
return list.size();
}

@Override
public boolean isEmpty() {
return list.isEmpty();
}

@Override
public boolean contains(Object o) {
return list.contains(o);
}

@Override
public Iterator<E> iterator() {
return list.iterator();
}

@Override
public Object[] toArray() {
return list.toArray();
}

@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}

@Override
public boolean add(E e) {
return list.add(e);
}

@Override
public boolean remove(Object o) {
return list.remove(o);
}

@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}

@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}

@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}

@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}

@Override
public void clear() {
list.clear();
}

@Override
public E get(int index) {
return list.get(index);
}

@Override
public E set(int index, E element) {
return list.set(index, element);
}

@Override
public void add(int index, E element) {
list.add(index, element);
}

@Override
public E remove(int index) {
return list.remove(index);
}

@Override
public int indexOf(Object o) {
return list.indexOf(o);
}

@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}

@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}

@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
}

----------------------------------------------------------------------------------------

1.概述

本文我們將重點介紹Spring中 @Valid@Validated註解的區別 。

驗證使用者輸入是否正確是我們應用程式中的常見功能。Spring提供了@Valid和@Validated兩個註解來實現驗證功能,下面我們來詳細介紹它們。

2. @Valid和@Validate註解

在Spring中,我們使用@Valid 註解進行方法級別驗證,同時還能用它來標記成員屬性以進行驗證。

但是,此註釋不支援分組驗證。@Validated則支援分組驗證。

3.例子

讓我們考慮一個使用Spring Boot開發的簡單使用者登錄檔單。首先,我們只有名稱密碼屬性:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    // standard constructors / setters / getters / toString

}

接下來,讓我們看一下控制器。在這裡,我們將使用帶有@Valid批註的saveBasicInfo方法來驗證使用者輸入:

@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)
public String saveBasicInfo(
  @Valid @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, 
  ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

現在讓我們測試一下這個方法:

@Test
public void givenSaveBasicInfo_whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

在確認測試成功執行之後,現在讓我們擴充套件功能。下一步的邏輯步驟是將其轉換為多步驟登錄檔格,就像大多數嚮導一樣。第一步,名稱密碼保持不變。在第二步中,我們將獲取其他資訊,例如agephone。因此,我們將使用以下其他欄位更新域物件:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    @Min(value = 18, message = "Age should not be less than 18")
    private int age;

    @NotBlank
    private String phone;

    // standard constructors / setters / getters / toString   

}

但是,這一次,我們將注意到先前的測試失敗。這是因為我們沒有傳遞年齡電話欄位。

為了支援此行為,我們引入支援分組驗證的@Validated批註。

分組驗證,就是將欄位分組,分別驗證,比如我們將使用者資訊分為兩組:BasicInfoAdvanceInfo

可以建立兩個空介面:

public interface BasicInfo {
}
public interface AdvanceInfo {
}

第一步將具有BasicInfo介面,第二步 將具有AdvanceInfo 。此外,我們將更新UserAccount類以使用這些標記介面,如下所示:

public class UserAccount {

    @NotNull(groups = BasicInfo.class)
    @Size(min = 4, max = 15, groups = BasicInfo.class)
    private String password;

    @NotBlank(groups = BasicInfo.class)
    private String name;

    @Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)
    private int age;

    @NotBlank(groups = AdvanceInfo.class)
    private String phone;

    // standard constructors / setters / getters / toString   

}

另外,我們現在將更新控制器以使用@Validated註釋而不是@Valid

@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)
public String saveBasicInfoStep1(
  @Validated(BasicInfo.class) 
  @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

更新後,再次執行測試,現在可以成功執行。現在,我們還要測試這個新方法:

@Test
public void givenSaveBasicInfoStep1`whenCorrectInput`thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

也成功執行!

接下來,讓我們看看@Valid對於觸發巢狀屬性驗證是必不可少的。

4.使用@Valid批註標記巢狀物件

@Valid 可以用於巢狀物件。例如,在我們當前的場景中,讓我們建立一個 UserAddress 物件:

public class UserAddress {

    @NotBlank
    private String countryCode;

    // standard constructors / setters / getters / toString
}

為了確保驗證此巢狀物件,我們將使用@Valid批註裝飾屬性:

public class UserAccount {

    //...

    @Valid
    @NotNull(groups = AdvanceInfo.class)
    private UserAddress useraddress;

    // standard constructors / setters / getters / toString 
}

5. 總結

@Valid保證了整個物件的驗證, 但是它是對整個物件進行驗證,當僅需要部分驗證的時候就會出現問題。 這時候,可以使用@Validated 進行分組驗證。

----------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------