亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

如何使用SpringBoot進(jìn)行優(yōu)雅的數(shù)據(jù)驗(yàn)證

 更新時(shí)間:2020年11月24日 09:34:27   作者:程序員自由之路  
這篇文章主要介紹了如何使用SpringBoot進(jìn)行優(yōu)雅的數(shù)據(jù)驗(yàn)證,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

JSR-303 規(guī)范

在程序進(jìn)行數(shù)據(jù)處理之前,對(duì)數(shù)據(jù)進(jìn)行準(zhǔn)確性校驗(yàn)是我們必須要考慮的事情。盡早發(fā)現(xiàn)數(shù)據(jù)錯(cuò)誤,不僅可以防止錯(cuò)誤向核心業(yè)務(wù)邏輯蔓延,而且這種錯(cuò)誤非常明顯,容易發(fā)現(xiàn)解決。

JSR303 規(guī)范(Bean Validation 規(guī)范)為 JavaBean 驗(yàn)證定義了相應(yīng)的元數(shù)據(jù)模型和 API。在應(yīng)用程序中,通過(guò)使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以確保數(shù)據(jù)模型(JavaBean)的正確性。constraint 可以附加到字段,getter 方法,類(lèi)或者接口上面。對(duì)于一些特定的需求,用戶(hù)可以很容易的開(kāi)發(fā)定制化的 constraint。Bean Validation 是一個(gè)運(yùn)行時(shí)的數(shù)據(jù)驗(yàn)證框架,在驗(yàn)證之后驗(yàn)證的錯(cuò)誤信息會(huì)被馬上返回。

關(guān)于 JSR 303 – Bean Validation 規(guī)范,可以參考官網(wǎng)

對(duì)于 JSR 303 規(guī)范,Hibernate Validator 對(duì)其進(jìn)行了參考實(shí)現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實(shí)現(xiàn),除此之外還有一些附加的 constraint。如果想了解更多有關(guān) Hibernate Validator 的信息,請(qǐng)查看官網(wǎng)。

Constraint 詳細(xì)信息
@AssertFalse 被注釋的元素必須為 false
@AssertTrue 同@AssertFalse
@DecimalMax 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于等于指定的最大值
@DecimalMin 同DecimalMax
@Digits 帶批注的元素必須是一個(gè)在可接受范圍內(nèi)的數(shù)字
@Email 顧名思義
@Future 將來(lái)的日期
@FutureOrPresent 現(xiàn)在或?qū)?lái)
@Max 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于等于指定的最大值
@Min 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于等于指定的最小值
@Negative 帶注釋的元素必須是一個(gè)嚴(yán)格的負(fù)數(shù)(0為無(wú)效值)
@NegativeOrZero 帶注釋的元素必須是一個(gè)嚴(yán)格的負(fù)數(shù)(包含0)
@NotBlank 同StringUtils.isNotBlank
@NotEmpty 同StringUtils.isNotEmpty
@NotNull 不能是Null
@Null 元素是Null
@Past 被注釋的元素必須是一個(gè)過(guò)去的日期
@PastOrPresent 過(guò)去和現(xiàn)在
@Pattern 被注釋的元素必須符合指定的正則表達(dá)式
@Positive 被注釋的元素必須嚴(yán)格的正數(shù)(0為無(wú)效值)
@PositiveOrZero 被注釋的元素必須嚴(yán)格的正數(shù)(包含0)
@Szie 帶注釋的元素大小必須介于指定邊界(包括)之間

Hibernate Validator 附加的 constraint

Constraint 詳細(xì)信息
@Email 被注釋的元素必須是電子郵箱地址
@Length 被注釋的字符串的大小必須在指定的范圍內(nèi)
@NotEmpty 被注釋的字符串的必須非空
@Range 被注釋的元素必須在合適的范圍內(nèi)
CreditCardNumber 被注釋的元素必須符合信用卡格式

Hibernate Validator 不同版本附加的 Constraint 可能不太一樣,具體還需要你自己查看你使用版本。Hibernate 提供的 Constraint在org.hibernate.validator.constraints這個(gè)包下面。

一個(gè) constraint 通常由 annotation 和相應(yīng)的 constraint validator 組成,它們是一對(duì)多的關(guān)系。也就是說(shuō)可以有多個(gè) constraint validator 對(duì)應(yīng)一個(gè) annotation。在運(yùn)行時(shí),Bean Validation 框架本身會(huì)根據(jù)被注釋元素的類(lèi)型來(lái)選擇合適的 constraint validator 對(duì)數(shù)據(jù)進(jìn)行驗(yàn)證。

有些時(shí)候,在用戶(hù)的應(yīng)用中需要一些更復(fù)雜的 constraint。Bean Validation 提供擴(kuò)展 constraint 的機(jī)制??梢酝ㄟ^(guò)兩種方法去實(shí)現(xiàn),一種是組合現(xiàn)有的 constraint 來(lái)生成一個(gè)更復(fù)雜的 constraint,另外一種是開(kāi)發(fā)一個(gè)全新的 constraint。

使用Spring Boot進(jìn)行數(shù)據(jù)校驗(yàn)

Spring Validation 對(duì) hibernate validation 進(jìn)行了二次封裝,可以讓我們更加方便地使用數(shù)據(jù)校驗(yàn)功能。這邊我們通過(guò) Spring Boot 來(lái)引用校驗(yàn)功能。

如果你用的 Spring Boot 版本小于 2.3.x,spring-boot-starter-web 會(huì)自動(dòng)引入 hibernate-validator 的依賴(lài)。如果 Spring Boot 版本大于 2.3.x,則需要手動(dòng)引入依賴(lài):

<dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-validator</artifactId>
 <version>6.0.1.Final</version>
</dependency>

直接參數(shù)校驗(yàn)

有時(shí)候接口的參數(shù)比較少,只有一個(gè)活著兩個(gè)參數(shù),這時(shí)候就沒(méi)必要定義一個(gè)DTO來(lái)接收參數(shù),可以直接接收參數(shù)。

@Validated
@RestController
@RequestMapping("/user")
public class UserController {

 private static Logger logger = LoggerFactory.getLogger(UserController.class);

 @GetMapping("/getUser")
 @ResponseBody
 // 注意:如果想在參數(shù)中使用 @NotNull 這種注解校驗(yàn),就必須在類(lèi)上添加 @Validated;
 public UserDTO getUser(@NotNull(message = "userId不能為空") Integer userId){
 logger.info("userId:[{}]",userId);
 UserDTO res = new UserDTO();
 res.setUserId(userId);
 res.setName("程序員自由之路");
 res.setAge(8);
 return res;
 }
}

下面是統(tǒng)一異常處理類(lèi)

@RestControllerAdvice
public class GlobalExceptionHandler {

 private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

 @ExceptionHandler(value = ConstraintViolationException.class)
 public Response handle1(ConstraintViolationException ex){
  StringBuilder msg = new StringBuilder();
 Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
 for (ConstraintViolation<?> constraintViolation : constraintViolations) {
  PathImpl pathImpl = (PathImpl) constraintViolation.getPropertyPath();
  String paramName = pathImpl.getLeafNode().getName();
  String message = constraintViolation.getMessage();
  msg.append("[").append(message).append("]");
 }
 logger.error(msg.toString(),ex);
 // 注意:Response類(lèi)必須有g(shù)et和set方法,不然會(huì)報(bào)錯(cuò)
 return new Response(RCode.PARAM_INVALID.getCode(),msg.toString());
 }

 @ExceptionHandler(value = Exception.class)
 public Response handle1(Exception ex){
 logger.error(ex.getMessage(),ex);
 return new Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
 }
}

調(diào)用結(jié)果

# 這里沒(méi)有傳userId
GET http://127.0.0.1:9999/user/getUser

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 14 Nov 2020 07:35:44 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
 "rtnCode": "1000",
 "rtnMsg": "[userId不能為空]"
}

實(shí)體類(lèi)DTO校驗(yàn)

定義一個(gè)DTO

import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotEmpty;

public class UserDTO {

 private Integer userId;

 @NotEmpty(message = "姓名不能為空")
 private String name;
 @Range(min = 18,max = 50,message = "年齡必須在18和50之間")
 private Integer age;
 //省略get和set方法
}

接收參數(shù)時(shí)使用@Validated進(jìn)行校驗(yàn)

@PostMapping("/saveUser")
@ResponseBody
//注意:如果方法中的參數(shù)是對(duì)象類(lèi)型,則必須要在參數(shù)對(duì)象前面添加 @Validated
public Response<UserDTO> getUser(@Validated @RequestBody UserDTO userDTO){
 userDTO.setUserId(100);
 Response response = Response.success();
 response.setData(userDTO);
 return response;
}

統(tǒng)一異常處理

@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Response handle2(MethodArgumentNotValidException ex){
 BindingResult bindingResult = ex.getBindingResult();
 if(bindingResult!=null){
 if(bindingResult.hasErrors()){
  FieldError fieldError = bindingResult.getFieldError();
  String field = fieldError.getField();
  String defaultMessage = fieldError.getDefaultMessage();
  logger.error(ex.getMessage(),ex);
  return new Response(RCode.PARAM_INVALID.getCode(),field+":"+defaultMessage);
 }else {
  logger.error(ex.getMessage(),ex);
  return new Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
 }
 }else {
 logger.error(ex.getMessage(),ex);
 return new Response(RCode.ERROR.getCode(),RCode.ERROR.getMsg());
 }
}

調(diào)用結(jié)果

### 創(chuàng)建用戶(hù)
POST http://127.0.0.1:9999/user/saveUser
Content-Type: application/json

{
 "name1": "程序員自由之路",
 "age": "18"
}

# 下面是返回結(jié)果
{
 "rtnCode": "1000",
 "rtnMsg": "姓名不能為空"
}

對(duì)Service層方法參數(shù)校驗(yàn)

個(gè)人不太喜歡這種校驗(yàn)方式,一半情況下調(diào)用service層方法的參數(shù)都需要在controller層校驗(yàn)好,不需要再校驗(yàn)一次。這邊列舉這個(gè)功能,只是想說(shuō) Spring 也支持這個(gè)。

@Validated
@Service
public class ValidatorService {

	private static final Logger logger = LoggerFactory.getLogger(ValidatorService.class);

	public String show(@NotNull(message = "不能為空") @Min(value = 18, message = "最小18") String age) {
		logger.info("age = {}", age);
		return age;
	}

}

分組校驗(yàn)

有時(shí)候?qū)τ诓煌慕涌冢枰獙?duì)DTO進(jìn)行不同的校驗(yàn)規(guī)則。還是以上面的UserDTO為列,另外一個(gè)接口可能不需要將age限制在18~50之間,只需要大于18就可以了。

這樣上面的校驗(yàn)規(guī)則就不適用了。分組校驗(yàn)就是來(lái)解決這個(gè)問(wèn)題的,同一個(gè)DTO,不同的分組采用不同的校驗(yàn)策略。

public class UserDTO {

 public interface Default {
 }

 public interface Group1 {
 }

 private Integer userId;
 //注意:@Validated 注解中加上groups屬性后,DTO中沒(méi)有加group屬性的校驗(yàn)規(guī)則將失效
 @NotEmpty(message = "姓名不能為空",groups = Default.class)
 private String name;

 //注意:加了groups屬性之后,必須在@Validated 注解中也加上groups屬性后,校驗(yàn)規(guī)則才能生效,不然下面的校驗(yàn)限制就失效了
 @Range(min = 18, max = 50, message = "年齡必須在18和50之間",groups = Default.class)
 @Range(min = 17, message = "年齡必須大于17", groups = Group1.class)
 private Integer age;
}

使用方式

@PostMapping("/saveUserGroup")
@ResponseBody
//注意:如果方法中的參數(shù)是對(duì)象類(lèi)型,則必須要在參數(shù)對(duì)象前面添加 @Validated
//進(jìn)行分組校驗(yàn),年齡滿(mǎn)足大于17
public Response<UserDTO> saveUserGroup(@Validated(value = {UserDTO.Group1.class}) @RequestBody UserDTO userDTO){
 userDTO.setUserId(100);
 Response response = Response.success();
 response.setData(userDTO);
 return response;
}

使用Group1分組進(jìn)行校驗(yàn),因?yàn)镈TO中,Group1分組對(duì)name屬性沒(méi)有校驗(yàn),所以這個(gè)校驗(yàn)將不會(huì)生效。

分組校驗(yàn)的好處是可以對(duì)同一個(gè)DTO設(shè)置不同的校驗(yàn)規(guī)則,缺點(diǎn)就是對(duì)于每一個(gè)新的校驗(yàn)分組,都需要重新設(shè)置下這個(gè)分組下面每個(gè)屬性的校驗(yàn)規(guī)則。

分組校驗(yàn)還有一個(gè)按順序校驗(yàn)功能。

考慮一種場(chǎng)景:一個(gè)bean有1個(gè)屬性(假如說(shuō)是attrA),這個(gè)屬性上添加了3個(gè)約束(假如說(shuō)是@NotNull、@NotEmpty、@NotBlank)。默認(rèn)情況下,validation-api對(duì)這3個(gè)約束的校驗(yàn)順序是隨機(jī)的。也就是說(shuō),可能先校驗(yàn)@NotNull,再校驗(yàn)@NotEmpty,最后校驗(yàn)@NotBlank,也有可能先校驗(yàn)@NotBlank,再校驗(yàn)@NotEmpty,最后校驗(yàn)@NotNull。

那么,如果我們的需求是先校驗(yàn)@NotNull,再校驗(yàn)@NotBlank,最后校驗(yàn)@NotEmpty。@GroupSequence注解可以實(shí)現(xiàn)這個(gè)功能。

public class GroupSequenceDemoForm {

 @NotBlank(message = "至少包含一個(gè)非空字符", groups = {First.class})
 @Size(min = 11, max = 11, message = "長(zhǎng)度必須是11", groups = {Second.class})
 private String demoAttr;

 public interface First {

 }

 public interface Second {

 }

 @GroupSequence(value = {First.class, Second.class})
 public interface GroupOrderedOne {
 // 先計(jì)算屬于 First 組的約束,再計(jì)算屬于 Second 組的約束
 }


 @GroupSequence(value = {Second.class, First.class})
 public interface GroupOrderedTwo {
 // 先計(jì)算屬于 Second 組的約束,再計(jì)算屬于 First 組的約束
 }

}

使用方式

// 先計(jì)算屬于 First 組的約束,再計(jì)算屬于 Second 組的約束
@Validated(value = {GroupOrderedOne.class}) @RequestBody GroupSequenceDemoForm form

嵌套校驗(yàn)

前面的示例中,DTO類(lèi)里面的字段都是基本數(shù)據(jù)類(lèi)型和String等類(lèi)型。

但是實(shí)際場(chǎng)景中,有可能某個(gè)字段也是一個(gè)對(duì)象,如果我們需要對(duì)這個(gè)對(duì)象里面的數(shù)據(jù)也進(jìn)行校驗(yàn),可以使用嵌套校驗(yàn)。

假如UserDTO中還用一個(gè)Job對(duì)象,比如下面的結(jié)構(gòu)。需要注意的是,在job類(lèi)的校驗(yàn)上面一定要加上@Valid注解。

public class UserDTO1 {

 private Integer userId;
 @NotEmpty
 private String name;
 @NotNull
 private Integer age;
 @Valid
 @NotNull
 private Job job;

 public Integer getUserId() {
 return userId;
 }

 public void setUserId(Integer userId) {
 this.userId = userId;
 }

 public String getName() {
 return name;
 }

 public void setName(String name) {
 this.name = name;
 }

 public Integer getAge() {
 return age;
 }

 public void setAge(Integer age) {
 this.age = age;
 }

 public Job getJob() {
 return job;
 }

 public void setJob(Job job) {
 this.job = job;
 }

 /**
 * 這邊必須設(shè)置成靜態(tài)內(nèi)部類(lèi)
 */
 static class Job {
 @NotEmpty
 private String jobType;
 @DecimalMax(value = "1000.99")
 private Double salary;

 public String getJobType() {
  return jobType;
 }

 public void setJobType(String jobType) {
  this.jobType = jobType;
 }

 public Double getSalary() {
  return salary;
 }

 public void setSalary(Double salary) {
  this.salary = salary;
 }
 }

}

使用方式

@PostMapping("/saveUserWithJob")
@ResponseBody
public Response<UserDTO1> saveUserWithJob(@Validated @RequestBody UserDTO1 userDTO){
 userDTO.setUserId(100);
 Response response = Response.success();
 response.setData(userDTO);
 return response;
}

測(cè)試結(jié)果

POST http://127.0.0.1:9999/user/saveUserWithJob
Content-Type: application/json

{
 "name": "程序員自由之路",
 "age": "16",
 "job": {
 "jobType": "1",
 "salary": "9999.99"
 }
}

{
 "rtnCode": "1000",
 "rtnMsg": "job.salary:必須小于或等于1000.99"
}

嵌套校驗(yàn)可以結(jié)合分組校驗(yàn)一起使用。還有就是嵌套集合校驗(yàn)會(huì)對(duì)集合里面的每一項(xiàng)都進(jìn)行校驗(yàn),例如List字段會(huì)對(duì)這個(gè)list里面的每一個(gè)Job對(duì)象都進(jìn)行校驗(yàn)。這個(gè)點(diǎn)
在下面的@Valid和@Validated的區(qū)別章節(jié)有詳細(xì)講到。

集合校驗(yàn)

如果請(qǐng)求體直接傳遞了json數(shù)組給后臺(tái),并希望對(duì)數(shù)組中的每一項(xiàng)都進(jìn)行參數(shù)校驗(yàn)。此時(shí),如果我們直接使用java.util.Collection下的list或者set來(lái)接收數(shù)據(jù),參數(shù)校驗(yàn)并不會(huì)生效!我們可以使用自定義list集合來(lái)接收參數(shù):

包裝List類(lèi)型,并聲明@Valid注解

public class ValidationList<T> implements List<T> {

 // @Delegate是lombok注解
 // 本來(lái)實(shí)現(xiàn)List接口需要實(shí)現(xiàn)一系列方法,使用這個(gè)注解可以委托給ArrayList實(shí)現(xiàn)
 // @Delegate
 @Valid
 public List list = new ArrayList<>();


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

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

 @Override
 public boolean contains(Object o) {
 return list.contains(o);
 }
 //.... 下面省略一系列List接口方法,其實(shí)都是調(diào)用了ArrayList的方法
}

調(diào)用方法

@PostMapping("/batchSaveUser")
@ResponseBody
public Response batchSaveUser(@Validated(value = UserDTO.Default.class) @RequestBody ValidationList<UserDTO> userDTOs){
 return Response.success();
}

調(diào)用結(jié)果

Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'list[1]' of bean class [com.csx.demo.spring.boot.dto.ValidationList]: Bean property 'list[1]' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
 at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:622) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
 at org.springframework.beans.AbstractNestablePropertyAccessor.getNestedPropertyAccessor(AbstractNestablePropertyAccessor.java:839) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
 at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyAccessorForPropertyPath(AbstractNestablePropertyAccessor.java:816) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
 at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:610) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]

會(huì)拋出NotReadablePropertyException異常,需要對(duì)這個(gè)異常做統(tǒng)一處理。這邊代碼就不貼了。

自定義校驗(yàn)器

在Spring中自定義校驗(yàn)器非常簡(jiǎn)單,分兩步走。

自定義約束注解

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

 // 默認(rèn)錯(cuò)誤消息
 String message() default "加密id格式錯(cuò)誤";

 // 分組
 Class[] groups() default {};

 // 負(fù)載
 Class[] payload() default {};
}

實(shí)現(xiàn)ConstraintValidator接口編寫(xiě)約束校驗(yàn)器

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

 private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

 @Override
 public boolean isValid(String value, ConstraintValidatorContext context) {
 // 不為null才進(jìn)行校驗(yàn)
 if (value != null) {
  Matcher matcher = PATTERN.matcher(value);
  return matcher.find();
 }
 return true;
 }
}

編程式校驗(yàn)

上面的示例都是基于注解來(lái)實(shí)現(xiàn)自動(dòng)校驗(yàn)的,在某些情況下,我們可能希望以編程方式調(diào)用驗(yàn)證。這個(gè)時(shí)候可以注入
javax.validation.Validator對(duì)象,然后再調(diào)用其api。

@Autowired
private javax.validation.Validator globalValidator;

// 編程式校驗(yàn)
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
 Set<constraintviolation> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
 // 如果校驗(yàn)通過(guò),validate為空;否則,validate包含未校驗(yàn)通過(guò)項(xiàng)
 if (validate.isEmpty()) {
 // 校驗(yàn)通過(guò),才會(huì)執(zhí)行業(yè)務(wù)邏輯處理

 } else {
 for (ConstraintViolation userDTOConstraintViolation : validate) {
  // 校驗(yàn)失敗,做其它邏輯
  System.out.println(userDTOConstraintViolation);
 }
 }
 return Result.ok();
}

快速失敗(Fail Fast)配置

Spring Validation默認(rèn)會(huì)校驗(yàn)完所有字段,然后才拋出異常。可以通過(guò)一些簡(jiǎn)單的配置,開(kāi)啟Fali Fast模式,一旦校驗(yàn)失敗就立即返回。

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

校驗(yàn)信息的國(guó)際化

Spring 的校驗(yàn)功能可以返回很友好的校驗(yàn)信息提示,而且這個(gè)信息支持國(guó)際化。

這塊功能暫時(shí)暫時(shí)不常用,具體可以參考這篇文章

@Validated和@Valid的區(qū)別聯(lián)系

首先,@Validated和@Valid都能實(shí)現(xiàn)基本的驗(yàn)證功能,也就是如果你是想驗(yàn)證一個(gè)參數(shù)是否為空,長(zhǎng)度是否滿(mǎn)足要求這些簡(jiǎn)單功能,使用哪個(gè)注解都可以。

但是這兩個(gè)注解在分組、注解作用的地方、嵌套驗(yàn)證等功能上兩個(gè)有所不同。下面列下這兩個(gè)注解主要的不同點(diǎn)。

  • @Valid注解是JSR303規(guī)范的注解,@Validated注解是Spring框架自帶的注解;
  • @Valid不具有分組校驗(yàn)功能,@Validate具有分組校驗(yàn)功能;
  • @Valid可以用在方法、構(gòu)造函數(shù)、方法參數(shù)和成員屬性(字段)上,@Validated可以用在類(lèi)型、方法和方法參數(shù)上。但是不能用在成員屬性(字段)上,兩者是否能用于成員屬性(字段)上直接影響能否提供嵌套驗(yàn)證的功能;
  • @Valid加在成員屬性上可以對(duì)成員屬性進(jìn)行嵌套驗(yàn)證,而@Validate不能加在成員屬性上,所以不具備這個(gè)功能。

這邊說(shuō)明下,什么叫嵌套驗(yàn)證。

我們現(xiàn)在有個(gè)實(shí)體叫做Item:

public class Item {

 @NotNull(message = "id不能為空")
 @Min(value = 1, message = "id必須為正整數(shù)")
 private Long id;

 @NotNull(message = "props不能為空")
 @Size(min = 1, message = "至少要有一個(gè)屬性")
 private List<Prop> props;
}

Item帶有很多屬性,屬性里面有:pid、vid、pidName和vidName,如下所示:

public class Prop {

 @NotNull(message = "pid不能為空")
 @Min(value = 1, message = "pid必須為正整數(shù)")
 private Long pid;

 @NotNull(message = "vid不能為空")
 @Min(value = 1, message = "vid必須為正整數(shù)")
 private Long vid;

 @NotBlank(message = "pidName不能為空")
 private String pidName;

 @NotBlank(message = "vidName不能為空")
 private String vidName;
}

屬性這個(gè)實(shí)體也有自己的驗(yàn)證機(jī)制,比如pid和vid不能為空,pidName和vidName不能為空等。
現(xiàn)在我們有個(gè)ItemController接受一個(gè)Item的入?yún)?,想要?duì)Item進(jìn)行驗(yàn)證,如下所示:

@RestController
public class ItemController {

 @RequestMapping("/item/add")
 public void addItem(@Validated Item item, BindingResult bindingResult) {
 doSomething();
 }
}

在上圖中,如果Item實(shí)體的props屬性不額外加注釋?zhuān)挥蠤NotNull和@Size,無(wú)論入?yún)⒉捎聾Validated還是@Valid驗(yàn)證,Spring Validation框架只會(huì)對(duì)Item的id和props做非空和數(shù)量驗(yàn)證,不會(huì)對(duì)props字段里的Prop實(shí)體進(jìn)行字段驗(yàn)證,也就是@Validated和@Valid加在方法參數(shù)前,都不會(huì)自動(dòng)對(duì)參數(shù)進(jìn)行嵌套驗(yàn)證。也就是說(shuō)如果傳的List中有Prop的pid為空或者是負(fù)數(shù),入?yún)Ⅱ?yàn)證不會(huì)檢測(cè)出來(lái)。

為了能夠進(jìn)行嵌套驗(yàn)證,必須手動(dòng)在Item實(shí)體的props字段上明確指出這個(gè)字段里面的實(shí)體也要進(jìn)行驗(yàn)證。由于@Validated不能用在成員屬性(字段)上,但是@Valid能加在成員屬性(字段)上,而且@Valid類(lèi)注解上也說(shuō)明了它支持嵌套驗(yàn)證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時(shí)并不能夠自動(dòng)進(jìn)行嵌套驗(yàn)證,而是用在需要嵌套驗(yàn)證類(lèi)的相應(yīng)字段上,來(lái)配合方法參數(shù)上@Validated或@Valid來(lái)進(jìn)行嵌套驗(yàn)證。

我們修改Item類(lèi)如下所示:

public class Item {

 @NotNull(message = "id不能為空")
 @Min(value = 1, message = "id必須為正整數(shù)")
 private Long id;

 @Valid // 嵌套驗(yàn)證必須用@Valid
 @NotNull(message = "props不能為空")
 @Size(min = 1, message = "props至少要有一個(gè)自定義屬性")
 private List<Prop> props;
}

然后我們?cè)贗temController的addItem函數(shù)上再使用@Validated或者@Valid,就能對(duì)Item的入?yún)⑦M(jìn)行嵌套驗(yàn)證。此時(shí)Item里面的props如果含有Prop的相應(yīng)字段為空的情況,Spring Validation框架就會(huì)檢測(cè)出來(lái),bindingResult就會(huì)記錄相應(yīng)的錯(cuò)誤。

Spring Validation原理簡(jiǎn)析

現(xiàn)在我們來(lái)簡(jiǎn)單分析下Spring校驗(yàn)功能的原理。

方法級(jí)別的參數(shù)校驗(yàn)實(shí)現(xiàn)原理

所謂的方法級(jí)別的校驗(yàn)就是指將@NotNull和@NotEmpty這些約束直接加在方法的參數(shù)上的。

比如

@GetMapping("/getUser")
@ResponseBody
public R getUser(@NotNull(message = "userId不能為空") Integer userId){
 //
}

或者

@Validated
@Service
public class ValidatorService {

	private static final Logger logger = LoggerFactory.getLogger(ValidatorService.class);

	public String show(@NotNull(message = "不能為空") @Min(value = 18, message = "最小18") String age) {
		logger.info("age = {}", age);
		return age;
	}

}

都屬于方法級(jí)別的校驗(yàn)。這種方式可用于任何Spring Bean的方法上,比如Controller/Service等。

其底層實(shí)現(xiàn)原理就是AOP,具體來(lái)說(shuō)是通過(guò)MethodValidationPostProcessor動(dòng)態(tài)注冊(cè)AOP切面,然后使用MethodValidationInterceptor對(duì)切點(diǎn)方法織入增強(qiáng)。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
 @Override
 public void afterPropertiesSet() {
 //為所有`@Validated`標(biāo)注的Bean創(chuàng)建切面
 Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
 //創(chuàng)建Advisor進(jìn)行增強(qiáng)
 this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
 }

 //創(chuàng)建Advice,本質(zhì)就是一個(gè)方法攔截器
 protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
 return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
 }
}

接著看一下MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
 @Override
 public Object invoke(MethodInvocation invocation) throws Throwable {
 //無(wú)需增強(qiáng)的方法,直接跳過(guò)
 if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
  return invocation.proceed();
 }
 //獲取分組信息
 Class[] groups = determineValidationGroups(invocation);
 ExecutableValidator execVal = this.validator.forExecutables();
 Method methodToValidate = invocation.getMethod();
 Set<constraintviolation> result;
 try {
  //方法入?yún)⑿r?yàn),最終還是委托給Hibernate Validator來(lái)校驗(yàn)
  result = execVal.validateParameters(
  invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
 }
 catch (IllegalArgumentException ex) {
  ...
 }
 //有異常直接拋出
 if (!result.isEmpty()) {
  throw new ConstraintViolationException(result);
 }
 //真正的方法調(diào)用
 Object returnValue = invocation.proceed();
 //對(duì)返回值做校驗(yàn),最終還是委托給Hibernate Validator來(lái)校驗(yàn)
 result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
 //有異常直接拋出
 if (!result.isEmpty()) {
  throw new ConstraintViolationException(result);
 }
 return returnValue;
 }
}

DTO級(jí)別的校驗(yàn)

@PostMapping("/saveUser")
@ResponseBody
//注意:如果方法中的參數(shù)是對(duì)象類(lèi)型,則必須要在參數(shù)對(duì)象前面添加 @Validated
public R saveUser(@Validated @RequestBody UserDTO userDTO){
 userDTO.setUserId(100);
 return R.SUCCESS.setData(userDTO);
}

這種屬于DTO級(jí)別的校驗(yàn)。在spring-mvc中,RequestResponseBodyMethodProcessor是用于解析@RequestBody標(biāo)注的參數(shù)以及處理@ResponseBody標(biāo)注方法的返回值的。顯然,執(zhí)行參數(shù)校驗(yàn)的邏輯肯定就在解析參數(shù)的方法resolveArgument()中。

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
 @Override
 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
     NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

 parameter = parameter.nestedIfOptional();
 //將請(qǐng)求數(shù)據(jù)封裝到DTO對(duì)象中
 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
 String name = Conventions.getVariableNameForParameter(parameter);

 if (binderFactory != null) {
  WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
  if (arg != null) {
  // 執(zhí)行數(shù)據(jù)校驗(yàn)
  validateIfApplicable(binder, parameter);
  if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
   throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
  }
  }
  if (mavContainer != null) {
  mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
  }
 }
 return adaptArgumentIfNecessary(arg, parameter);
 }
}

可以看到,resolveArgument()調(diào)用了validateIfApplicable()進(jìn)行參數(shù)校驗(yàn)。

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
 // 獲取參數(shù)注解,比如@RequestBody、@Valid、@Validated
 Annotation[] annotations = parameter.getParameterAnnotations();
 for (Annotation ann : annotations) {
 // 先嘗試獲取@Validated注解
 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
 //如果直接標(biāo)注了@Validated,那么直接開(kāi)啟校驗(yàn)。
 //如果沒(méi)有,那么判斷參數(shù)前是否有Valid起頭的注解。
 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
  Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
  Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
  //執(zhí)行校驗(yàn)
  binder.validate(validationHints);
  break;
 }
 }
}

看到這里,大家應(yīng)該能明白為什么這種場(chǎng)景下@Validated、@Valid兩個(gè)注解可以混用。我們接下來(lái)繼續(xù)看WebDataBinder.validate()實(shí)現(xiàn)。

最終發(fā)現(xiàn)底層最終還是調(diào)用了Hibernate Validator進(jìn)行真正的校驗(yàn)處理。

404等錯(cuò)誤的統(tǒng)一處理

參考博客

參考

Spring Validation實(shí)現(xiàn)原理及如何運(yùn)用

SpringBoot參數(shù)校驗(yàn)和國(guó)際化使用

@Valid和@Validated區(qū)別S

pring Validation最佳實(shí)踐及其實(shí)現(xiàn)原理,參數(shù)校驗(yàn)沒(méi)那么簡(jiǎn)單!

到此這篇關(guān)于如何使用SpringBoot進(jìn)行優(yōu)雅的數(shù)據(jù)驗(yàn)證的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java+Springboot搭建一個(gè)在線網(wǎng)盤(pán)文件分享系統(tǒng)

    Java+Springboot搭建一個(gè)在線網(wǎng)盤(pán)文件分享系統(tǒng)

    本主要介紹了通過(guò)springboot+freemark+jpa+MySQL實(shí)現(xiàn)的在線網(wǎng)盤(pán)文件分享系統(tǒng),其功能跟百度網(wǎng)盤(pán)非常類(lèi)似,可以實(shí)現(xiàn)文件的上傳、移動(dòng)、復(fù)制、下載等,需要的可以參考一下
    2021-11-11
  • 詳解SpringBoot之訪問(wèn)靜態(tài)資源(webapp...)

    詳解SpringBoot之訪問(wèn)靜態(tài)資源(webapp...)

    這篇文章主要介紹了詳解SpringBoot之訪問(wèn)靜態(tài)資源(webapp...),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • Java?Valhalla?Project項(xiàng)目介紹

    Java?Valhalla?Project項(xiàng)目介紹

    這篇文章主要介紹了Java?Valhalla?Project項(xiàng)目介紹,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-09-09
  • Java中的OpenTracing使用實(shí)例

    Java中的OpenTracing使用實(shí)例

    這篇文章主要介紹了Java中的OpenTracing使用實(shí)例,主要的OpenTracing API將所有主要組件聲明為接口以及輔助類(lèi),例如Tracer,Span,SpanContext,Scope,ScopeManager,Format(用映射定義通用的SpanContext注入和提取格式),需要的朋友可以參考下
    2024-01-01
  • SpringBoot中自定義注解實(shí)現(xiàn)控制器訪問(wèn)次數(shù)限制實(shí)例

    SpringBoot中自定義注解實(shí)現(xiàn)控制器訪問(wèn)次數(shù)限制實(shí)例

    本篇文章主要介紹了SpringBoot中自定義注解實(shí)現(xiàn)控制器訪問(wèn)次數(shù)限制實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。
    2017-04-04
  • 使用ResponseEntity處理API返回問(wèn)題

    使用ResponseEntity處理API返回問(wèn)題

    這篇文章主要介紹了使用ResponseEntity處理API返回問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • 盤(pán)點(diǎn)Java中延時(shí)任務(wù)的多種實(shí)現(xiàn)方式

    盤(pán)點(diǎn)Java中延時(shí)任務(wù)的多種實(shí)現(xiàn)方式

    當(dāng)需要一個(gè)定時(shí)發(fā)布系統(tǒng)通告的功能,如何實(shí)現(xiàn)??當(dāng)支付超時(shí),訂單自動(dòng)取消,如何實(shí)現(xiàn)?其實(shí)這些問(wèn)題本質(zhì)都是延時(shí)任務(wù)的實(shí)現(xiàn),本文為大家盤(pán)點(diǎn)了多種常見(jiàn)的延時(shí)任務(wù)實(shí)現(xiàn)方法,希望對(duì)大家有所幫助
    2022-12-12
  • java中靜態(tài)導(dǎo)入機(jī)制用法實(shí)例詳解

    java中靜態(tài)導(dǎo)入機(jī)制用法實(shí)例詳解

    這篇文章主要介紹了java中靜態(tài)導(dǎo)入機(jī)制用法實(shí)例詳解的相關(guān)資料,需要的朋友可以參考下
    2017-07-07
  • Spring實(shí)戰(zhàn)之獲取方法返回值操作示例

    Spring實(shí)戰(zhàn)之獲取方法返回值操作示例

    這篇文章主要介紹了Spring實(shí)戰(zhàn)之獲取方法返回值操作,涉及spring配置文件與方法返回值操作相關(guān)使用技巧,需要的朋友可以參考下
    2019-12-12
  • SpringBoot項(xiàng)目中Druid自動(dòng)登錄功能實(shí)現(xiàn)

    SpringBoot項(xiàng)目中Druid自動(dòng)登錄功能實(shí)現(xiàn)

    Druid是Java語(yǔ)言中最好的數(shù)據(jù)庫(kù)連接池,Druid能夠提供強(qiáng)大的監(jiān)控和擴(kuò)展功能,這篇文章主要介紹了SpringBoot項(xiàng)目中Druid自動(dòng)登錄功能實(shí)現(xiàn),需要的朋友可以參考下
    2024-08-08

最新評(píng)論