spring-boot-@Valid和@Validated詳解
----------------------------------------------------------------------------------------
在實際的專案開發中,經常會遇到對引數進行校驗的場景,最常見的就是後端需要對前端傳過來的資料進行校驗。
我理解的資料校驗大致分為兩類:
一類是對資料本身進行校驗,不涉及與資料庫互動的,比如正則校驗、非空校驗、指定的列舉資料、最大值、最小值等等。
二類是資料的校驗需要和資料庫互動的,比如是否唯一(資料庫中是否存在)、數量限制(資料庫中只能允許存在10條資料)等等。
由於第二類其實屬於業務邏輯,這裡不做討論,本文主要是針對第一類場景的資料校驗。
其實也可以在業務程式碼中去做校驗判斷,但是這樣就不夠優雅了不是嗎,話不多說直接開始正文
1、@Valid和@Validated介紹以及對應的Maven座標
4、@Valid的巢狀校驗(校驗的物件中引入的其他物件或者List物件的校驗)
5、@Validated的分組校驗(不同的分組不同的校驗策略)
6、@Validated中的分組校驗時@GroupSequence使用(指定欄位的校驗順序)
7、快速失敗機制(單個引數校驗失敗後,不再對剩下的引數進行校驗)
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(成員),彼此的關係是,一個專案中存在一個團隊,一個團隊中存在多個成員,實體類裡面的屬性虛構,目的是為了舉例校驗的相關注解。
@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;
}
@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());
}
在確認測試成功執行之後,現在讓我們擴充套件功能。下一步的邏輯步驟是將其轉換為多步驟登錄檔格,就像大多數嚮導一樣。第一步,名稱
和密碼
保持不變。在第二步中,我們將獲取其他資訊,例如age
和 phone
。因此,我們將使用以下其他欄位更新域物件:
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
批註。
分組驗證
,就是將欄位分組,分別驗證,比如我們將使用者資訊分為兩組:BasicInfo
和AdvanceInfo
可以建立兩個空介面:
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
進行分組驗證。
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------