Valid 和 Validated 的區別
區別 | Valid | Validated |
---|---|---|
提供者 | JSR-303規范 | Spring |
是否支持分組 | 不支持 | 支持 |
標注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
嵌套校驗 | 支持 | 不支持 |
引入依賴
如果spring-boot版本小於2.3.x,spring-boot-starter-web會自動傳入hibernate-validator依賴。
如果spring-boot版本大於2.3.x,則需要手動引入依賴:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.1.Final</version>
</dependency>
預定義對象的說明
Result結果
接口統一返回Result格式的結果:
package com.qiankai.valid.common;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 返回結果
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:38
*/
@Data
@Accessors(chain = true)
public class Result<T> {
private int code;
private String message;
private T data;
public boolean ok() {
return this.code == 0;
}
public static <T> Result<T> success() {
return new Result<T>().setCode(0).setMessage("成功");
}
public static <T> Result<T> success(T data) {
return new Result<T>().setCode(0).setMessage("成功").setData(data);
}
public static <T> Result<T> failure() {
return new Result<T>().setCode(-1).setMessage("失敗");
}
public static <T> Result<T> failure(int code, String msg) {
return new Result<T>().setCode(code).setMessage(msg);
}
public static <T> Result<T> failure(int code, String msg, T data) {
return new Result<T>().setCode(-1).setMessage("失敗").setData(data);
}
}
ErrorCode
全局異常錯誤碼,后面統一處理異常會用到:
一般為了更好的處理全局異常,使用的錯誤碼都是定義成枚舉類型(包含錯誤碼和錯誤描述),我這邊方便演示就隨便定義了一個類
package com.qiankai.valid.common;
/**
* 錯誤碼
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:08
*/
public final class ErrorCode {
/**
* 參數校驗失敗錯誤碼
*/
public static final int ARGUMENT_VALID_FAILURE = -2;
}
常用參數校驗
在DTO上添加注解,實現參數校驗,假設存在 UserDTO 如下:
@Data
public class UserDTO {
private Long userId;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}
RequestBody校驗
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* RequestBody 參數校驗
* 校驗失敗會拋出 MethodArgumentNotValidException 異常
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:37
*/
@RequestMapping("/api/user01")
@RestController
public class User01Controller {
/**
* RequestBody 參數校驗
* 使用 @Valid 和 @Validated 都可以
*/
@PostMapping("/save/1")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
return Result.success();
}
@PostMapping("/save/2")
public Result save2User(@RequestBody @Valid UserDTO userDTO) {
return Result.success();
}
}
RequestParam / PathVariable 校驗
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* RequestMapping / PathVariable 參數校驗
* 校驗失敗會拋出 ConstraintViolationException 異常
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 10:57
*/
@RequestMapping("/api/user02")
@RestController
@Validated
public class User02Controller {
/**
* 此時必須在Controller上標注 @Validated 注解,並在入參上聲明約束注解
*/
/**
* 路徑變量
* 添加約束注解 @Min
*/
@GetMapping("{userId}")
public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
// 校驗通過,才會執行業務邏輯處理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(userId);
userDTO.setAccount("11111111111111111");
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.success(userDTO);
}
/**
* 查詢參數
* 添加約束注解 @Length @NotNull
*/
@GetMapping("getByAccount")
public Result getByAccount(@Length(min = 6, max = 20) @NotNull String account) {
// 校驗通過,才會執行業務邏輯處理
UserDTO userDTO = new UserDTO();
userDTO.setUserId(10000000000000003L);
userDTO.setAccount(account);
userDTO.setUserName("xixi");
userDTO.setAccount("11111111111111111");
return Result.success(userDTO);
}
}
全局異常處理
上面如果校驗失敗,會拋出 MethodArgumentNotValidException
或者 ConstraintViolationException
異常。
在實際項目開發中,通常會用統一異常處理來返回一個更友好的提示。
比如我們系統要求無論發送什么異常,http的狀態碼必須返回200,由業務碼去區分系統的異常情況。
package com.qiankai.valid.exception;
import com.qiankai.valid.common.ErrorCode;
import com.qiankai.valid.common.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
/**
* 統一異常處理
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:05
*/
@RestControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校驗失敗:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
}
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Result handleConstraintViolationException(ConstraintViolationException ex) {
return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
}
}
使用全局異常前
使用全局異常后
分組校驗
有時候,為了區分業務場景,對於不同場景下的數據驗證規則可能不一樣(例如新增時可以不用傳遞 ID,而修改時必須傳遞ID),可以使用分組校驗。
代碼示例
DTO 如下:
在約束注解上聲明適用的分組
package com.qiankai.valid.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 分組校驗
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:12
*/
@Data
public class UserGroupValidDTO {
@NotNull(groups = Update.class)
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 保存的時候校驗分組
*/
public interface Save {
}
/**
* 更新的時候校驗分組
*/
public interface Update {
}
}
Controller 如下:
Validated注解上指定校驗的分組
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 分組校驗
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 11:19
*/
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
// 校驗通過,才會執行業務邏輯處理
return Result.success();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
// 校驗通過,才會執行業務邏輯處理
return Result.success();
}
}
嵌套校驗
上面的校驗主要是針對基本類型進行了校驗,如果DTO中包含了自定義的實體類,就需要用到嵌套校驗。
代碼示例
DTO 如下:
package com.qiankai.valid.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 嵌套校驗
* DTO中的某個字段也是一個對象,這種情況下,可以使用嵌套校驗
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 12:21
*/
@Data
public class UserNestedValidDTO {
@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;
/**
* 此時DTO類的對應字段必須標記@Valid注解
*/
@Valid
@NotNull(groups = {Save.class, Update.class})
private Job job;
@Data
public static class Job {
@NotNull(groups = {Update.class})
@Min(value = 1, groups = Update.class)
private Long jobId;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;
@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}
/**
* 保存的時候校驗分組
*/
public interface Save {
}
/**
* 更新的時候校驗分組
*/
public interface Update {
}
}
Controller 如下:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserNestedValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 嵌套校驗
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 13:31
*/
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
// 校驗通過,才會執行業務邏輯處理
return Result.success();
}
@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
// 校驗通過,才會執行業務邏輯處理
return Result.success();
}
}
接口調用示例
根據DTO以及Controller中的校驗規則,在update時,如果不傳 jobId
嵌套校驗就會報錯,如下:
集合校驗
如果請求體直接傳遞了json數組給后台,並希望對數組中的每一項都進行參數校驗。
此時,如果我們直接使用java.util.Collection下的list或者set來接收數據,參數校驗並不會生效!我們可以使用自定義list集合來接收參數:
代碼示例
先自定義一個用於包裝List的集合
package com.qiankai.valid.entity;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Delegate;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
/**
* 包裝 List類型,並聲明 @Valid 注解
* @param <E>
*/
@Getter
@Setter
public class ValidationList<E> implements List<E> {
@Delegate // @Delegate是lombok注解
@Valid // 一定要加@Valid注解
public List<E> list = new ArrayList<>();
// 一定要記得重寫toString方法
@Override
public String toString() {
return list.toString();
}
}
@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校驗不通過,會拋出
NotReadablePropertyException
,同樣可以使用統一異常進行處理。
Controller 如下:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import com.qiankai.valid.entity.ValidationList;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 集合校驗
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 14:14
*/
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
// 校驗通過,才會執行業務邏輯處理
return Result.success();
}
}
調用結果
編程式校驗
上面都是通過注解來進行校驗,也可以使用編程的方式進行校驗:
Controller:
package com.qiankai.valid.controller;
import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.ConstraintViolation;
import java.util.Set;
/**
* 編程式校驗參數
*
* @author kai_qian
* @version v1.0
* @since 2020/09/08 15:34
*/
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
@Autowired
private javax.validation.Validator globalValidator;
/**
* 編程式校驗
*/
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
// 如果校驗通過,validate為空;否則,validate包含未校驗通過項
if (validate.isEmpty()) {
// 校驗通過,才會執行業務邏輯處理
} else {
for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
// 校驗失敗,做其它邏輯
System.out.println(userGroupValidDTOConstraintViolation);
// throw new RuntimeException();
}
}
return Result.success();
}
}
結果如下:
配置快速失敗
Spring Validation默認會校驗完所有字段,然后才拋出異常。可以通過一些簡單的配置,開啟Fali Fast模式,一旦校驗失敗就立即返回。
/**
* 配置快速失敗
*/
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失敗模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}