最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

如何使用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 方法,類或者接口上面。對(duì)于一些特定的需求,用戶可以很容易的開(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)選擇合適的 constraint validator 對(duì)數(shù)據(jù)進(jìn)行驗(yàn)證。

有些時(shí)候,在用戶的應(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 的依賴。如果 Spring Boot 版本大于 2.3.x,則需要手動(dòng)引入依賴:

<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),就必須在類上添加 @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)一異常處理類

@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類必須有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í)體類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ì)象類型,則必須要在參數(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)建用戶
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ì)象類型,則必須要在參數(shù)對(duì)象前面添加 @Validated
//進(jìn)行分組校驗(yà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類里面的字段都是基本數(shù)據(jù)類型和String等類型。

但是實(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類的校驗(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)部類
 */
 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類型,并聲明@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接口編寫約束校驗(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)度是否滿足要求這些簡(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可以用在類型、方法和方法參數(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屬性不額外加注釋,只有@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類注解上也說(shuō)明了它支持嵌套驗(yàn)證功能,那么我們能夠推斷出:@Valid加在方法參數(shù)時(shí)并不能夠自動(dòng)進(jìn)行嵌套驗(yàn)證,而是用在需要嵌套驗(yàn)證類的相應(yīng)字段上,來(lái)配合方法參數(shù)上@Validated或@Valid來(lái)進(jìn)行嵌套驗(yàn)證。

我們修改Item類如下所示:

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ì)象類型,則必須要在參數(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)文章

  • 關(guān)于Java?CPU或內(nèi)存使用率過(guò)高問(wèn)題定位

    關(guān)于Java?CPU或內(nèi)存使用率過(guò)高問(wèn)題定位

    Spring?cloud微服務(wù)廣泛應(yīng)用后,服務(wù)的監(jiān)控和運(yùn)維壓力也與日俱增,經(jīng)常有服務(wù)出現(xiàn)CPU或者內(nèi)存使用率過(guò)高的告警,那么遇到這樣的問(wèn)題我們?cè)撊绾闻挪槟??我們可以借助哪些工具?lái)定位問(wèn)題呢?本文將介紹一下遇到此類問(wèn)題的解決思路和方法
    2024-10-10
  • 一文帶你了解FastExcel的使用

    一文帶你了解FastExcel的使用

    我們知道?EasyExcel?在作者從阿里離職之后就停止維護(hù)了,但在前兩周?EasyExcel?原作者推出了他的升級(jí)版框架?FastExcel,下面小編就來(lái)和大家聊聊FastExcel的具體使用吧
    2024-12-12
  • SpringCloud中使用webclient(get和post)請(qǐng)求微服務(wù)接口數(shù)據(jù)

    SpringCloud中使用webclient(get和post)請(qǐng)求微服務(wù)接口數(shù)據(jù)

    在SpringCloud項(xiàng)目中使用WebClient調(diào)用微服務(wù)時(shí),涉及配置WebClient、發(fā)起get和post請(qǐng)求等操作,如請(qǐng)求頭設(shè)置、服務(wù)地址配置、數(shù)據(jù)轉(zhuǎn)換處理、異常處理等,避免在循環(huán)中使用WebClient請(qǐng)求、路徑設(shè)置細(xì)節(jié)以及數(shù)據(jù)返回處理技巧,本文旨在幫助理解和應(yīng)用WebClient進(jìn)行微服務(wù)調(diào)用
    2024-10-10
  • Java上傳文件錯(cuò)誤java.lang.NoSuchMethodException的解決辦法

    Java上傳文件錯(cuò)誤java.lang.NoSuchMethodException的解決辦法

    今天小編就為大家分享一篇關(guān)于Java上傳文件錯(cuò)誤java.lang.NoSuchMethodException的解決辦法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧
    2019-01-01
  • SpringMVC中的HandlerMapping和HandlerAdapter詳解

    SpringMVC中的HandlerMapping和HandlerAdapter詳解

    這篇文章主要介紹了SpringMVC中的HandlerMapping和HandlerAdapter詳解,在Spring MVC中,HandlerMapping(處理器映射器)用于確定請(qǐng)求處理器對(duì)象,請(qǐng)求處理器可以是任何對(duì)象,只要它們使用了@Controller注解或注解@RequestMapping,需要的朋友可以參考下
    2023-08-08
  • 一道Java集合框架題 多種解題思路

    一道Java集合框架題 多種解題思路

    這篇文章主要介紹了一道Java集合框架題,多種解題思路,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-10-10
  • SpringBoot結(jié)合WebSocket實(shí)現(xiàn)聊天功能

    SpringBoot結(jié)合WebSocket實(shí)現(xiàn)聊天功能

    本文介紹了如何使用SpringBoot和WebSocket實(shí)現(xiàn)一個(gè)簡(jiǎn)單的聊天功能,包括導(dǎo)入依賴、配置類、創(chuàng)建消息實(shí)體、指定ServerEndpoint、創(chuàng)建客戶端等步驟,通過(guò)具體示例,演示了如何發(fā)送個(gè)人消息和群發(fā)消息,實(shí)現(xiàn)了基本的聊天功能,適合需要在項(xiàng)目中實(shí)現(xiàn)實(shí)時(shí)通訊功能的開(kāi)發(fā)者參考
    2024-11-11
  • Spring:@Async注解和AsyncResult與CompletableFuture使用問(wèn)題

    Spring:@Async注解和AsyncResult與CompletableFuture使用問(wèn)題

    這篇文章主要介紹了Spring:@Async注解和AsyncResult與CompletableFuture使用問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-08-08
  • Spring JdbcTemplate執(zhí)行數(shù)據(jù)庫(kù)操作詳解

    Spring JdbcTemplate執(zhí)行數(shù)據(jù)庫(kù)操作詳解

    JdbcTemplate是Spring框架自帶的對(duì)JDBC操作的封裝,目的是提供統(tǒng)一的模板方法使對(duì)數(shù)據(jù)庫(kù)的操作更加方便、友好,效率也不錯(cuò),這篇文章主要介紹了Spring JdbcTemplate執(zhí)行數(shù)據(jù)庫(kù)操作,需要的朋友可以參考下
    2022-10-10
  • SpringBoot下使用定時(shí)任務(wù)的方式全揭秘(6種)

    SpringBoot下使用定時(shí)任務(wù)的方式全揭秘(6種)

    這篇文章主要介紹了SpringBoot下使用定時(shí)任務(wù)的方式全揭秘(6種),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2019-02-02

最新評(píng)論

平乐县| 郸城县| 锡林浩特市| 岫岩| 昆山市| 台东县| 塔城市| 乐平市| 青阳县| 马尔康县| 辛集市| 健康| 南投县| 北票市| 安泽县| 河间市| 皋兰县| 庆城县| 恩平市| 小金县| 蚌埠市| 高淳县| 英吉沙县| 科技| 砚山县| 广汉市| 梓潼县| 东海县| 永靖县| 休宁县| 拉萨市| 温州市| 奉节县| 寿光市| 隆德县| 驻马店市| 内丘县| 云安县| 嘉禾县| 黑山县| 梅州市|