请不要再使用判断进行参数校验了
1. 前言
因為網絡傳輸的不可靠性,以及前端數據控制的可篡改性,后端的參數校驗是必須的,應用程序必須通過某種手段來確保輸入進來的數據從語義上來講是正確的。
2. 數據校驗的痛點
為了保證數據語義的正確,我們需要進行大量的判斷來處理驗證邏輯。而且項目的分層也會造成一些重復的校驗,產生大量與業務無關的代碼。不利于代碼的維護,增加了開發人員的工作量。
3. JSR 303 校驗規范及其實現
為了解決上面的痛點,將驗證邏輯與相應的領域模型進行綁定是十分有必要的。為此產生了JSR 303 – Bean Validation 規范。Hibernate Validator 是JSR-303的參考實現,它提供了JSR 303規范中所有的約束(constraint)的實現,同時也增加了一些擴展。
Hibernate Validator 提供的常用約束注解
| @Null | 被注釋的元素必須為 null |
| @NotNull | 被注釋的元素必須不為 null |
| @AssertTrue | 被注釋的元素必須為 true |
| @AssertFalse | 被注釋的元素必須為 false |
| @Min(value) | 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值 |
| @Max(value) | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @DecimalMin(value) | 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值 |
| @DecimalMax(value) | 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值 |
| @Size(max, min) | 被注釋的元素的大小必須在指定的范圍內 |
| @Digits (integer, fraction) | 被注釋的元素必須是一個數字,其值必須在可接受的范圍內 |
| @Past | 被注釋的元素必須是一個過去的日期 |
| @Future | 被注釋的元素必須是一個將來的日期 |
| @Pattern(value) | 被注釋的元素必須符合指定的正則表達式 |
| 被注釋的元素必須是電子郵箱地址 | |
| @Length | 被注釋的字符串的大小必須在指定的范圍內 |
| @NotEmpty | 被注釋的字符串的必須非空 |
| @Range | 被注釋的元素必須在合適的范圍內 |
4. 驗證注解的使用
在Spring Boot開發中使用Hibernate Validator是非常容易的,引入下面的starter就可以了:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId> </dependency>一種可以實現接口來定制Validator,一種是使用約束注解。胖哥覺得注解可以滿足絕大部分的需求,所以建議使用注解來進行數據校驗。而且注解更加靈活,控制的粒度也更加細。接下來我們來學習如何使用注解進行數據校驗。
4.1 約束注解的基本使用
我們對需要校驗的方法入參進行注解約束標記,例子如下:
@Data public?class?Student?{@NotBlank(message?=?"姓名必須填")private?String?name;@NotNull(message?=?"年齡必須填寫")@Range(min?=?1,max?=50,?message?=?"年齡取值范圍1-50")private?Integer?age;@NotEmpty(message?=?"成績必填")private?List<Double>?scores; }POST 請求
然后定義一個POST請求的Spring MVC接口:
@RestController @RequestMapping("/student") public?class?StudentController?{@PostMapping("/add")public?Rest<?>?addStudent(@Valid?@RequestBody?Student?student)?{return?RestBody.okData(student);} }通過對addStudent方法入參添加@Valid來啟用參數校驗。當使用下面數據進行請求將會拋出MethodArgumentNotValidException異常,提示age范圍超出1-50。
POST /student/add HTTP/1.1 Host: localhost:8888 Content-Type: application/json{"name": "felord.cn","age": 77,"scores": [55] }GET 請求
如法炮制,我們定義一個GET請求的接口:
@GetMapping("/get") public?Rest<?>?getStudent(@Valid?Student?student)?{return?RestBody.okData(student); }使用下面的請求可以正確對學生分數scores進行了校驗,但是拋出的并不是MethodArgumentNotValidException異常,而是BindException異常。這和使用@RequestBody注解有關系,這對我們后面的統一處理非常十分重要。
GET /student/get?name=felord.cn&age=12 HTTP/1.1 Host: localhost:8888自定義注解
可能有些同學注意到上面的年齡我進行了這樣的標記:
@NotNull(message?=?"年齡必須填寫") @Range(min?=?1,max?=50,?message?=?"年齡取值范圍1-50") private?Integer?age;這是因為@Range不會去校驗為空的情況,它只處理非空的時候是否符合范圍約束。所以要用多個注解來約束。如果我們某些場景需要重復的捆綁多個注解來使用時,可以使用自定義注解將它們封裝起來組合使用,下面這個注解就是將@NotNull和@Range進行了組合,你可以仿一個出來用用看。
import?org.hibernate.validator.constraints.Range;import?javax.validation.Constraint; import?javax.validation.Payload; import?javax.validation.ReportAsSingleViolation; import?javax.validation.constraints.NotNull; import?javax.validation.constraintvalidation.SupportedValidationTarget; import?javax.validation.constraintvalidation.ValidationTarget; import?java.lang.annotation.*;/***?@author?a*?@since?17:31**/ @Constraint(validatedBy?=?{} ) @SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT}) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,?ElementType.FIELD,ElementType.ANNOTATION_TYPE,?ElementType.CONSTRUCTOR,ElementType.PARAMETER,?ElementType.TYPE_USE}) @NotNull @Range(min?=?1,?max?=?50) @Documented @ReportAsSingleViolation public?@interface?Age?{//?message?必須有String?message()?default?"年齡必須填寫,且范圍為?1-50?";//?可選Class<?>[]?groups()?default?{};//?可選Class<??extends?Payload>[]?payload()?default?{}; }還有一種情況,我們在后臺定義了枚舉值來進行狀態的流轉,也是需要校驗的,比如我們定義了顏色枚舉:
public?enum?Colors?{RED,?YELLOW,?BLUE}我們希望入參不能超出Colors的范圍["RED", "YELLOW", "BLUE"],這就需要實現ConstraintValidator<A extends Annotation, T>接口來定義一個顏色約束了,其中泛型A為自定義的約束注解,泛型T為入參的類型,這里使用字符串,然后我們的實現如下:
/***?@author?felord.cn*?@since?17:57**/ public?class?ColorConstraintValidator?implements?ConstraintValidator<Color,?String>?{private?static?final?Set<String>?COLOR_CONSTRAINTS?=?new?HashSet<>();@Overridepublic?void?initialize(Color?constraintAnnotation)?{Colors[]?value?=?constraintAnnotation.value();List<String>?list?=?Arrays.stream(value).map(Enum::name).collect(Collectors.toList());COLOR_CONSTRAINTS.addAll(list);}@Overridepublic?boolean?isValid(String?value,?ConstraintValidatorContext?context)?{return?COLOR_CONSTRAINTS.contains(value);} }然后聲明對應的約束注解Color,需要在元注解@Constraint中指明使用上面定義好的處理類ColorConstraintValidator進行校驗。
/***?@author?felord.cn*?@since?17:55**/ @Constraint(validatedBy?=?ColorConstraintValidator.class) @Documented @Target({ElementType.METHOD,?ElementType.FIELD,ElementType.ANNOTATION_TYPE,?ElementType.CONSTRUCTOR,ElementType.PARAMETER,?ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public?@interface?Color?{//?錯誤提示信息String?message()?default?"顏色不符合規格";Class<?>[]?groups()?default?{};Class<??extends?Payload>[]?payload()?default?{};//?約束的類型Colors[]?value(); }然后我們來試一下,先對參數進行約束:
@Data public?class?Param?{@Color({Colors.BLUE,Colors.YELLOW})private?String?color; }接口跟上面幾個一樣,調用下面的接口將拋出BindException異常:
GET /student/color?color=CAY HTTP/1.1 Host: localhost:8888當我們把參數color賦值為BLUE或者YELLOW后,能夠成功得到響應。
4.2 常見問題
在實際使用起來我們會遇到一些問題,這里總結了一些常見的問題和處理方式。
檢驗基礎類型不生效的問題
上面為了校驗顏色我們聲明了一個Param對象來包裝唯一的字符串參數color,為什么直接使用下面的方式定義呢?
@GetMapping("/color") public?Rest<?>?color(@Valid?@Color({Colors.BLUE,Colors.YELLOW})?String?color)?{return?RestBody.okData(color); }或者使用路徑變量:
@GetMapping("/rest/{color}") public?Rest<?>?rest(@Valid?@Color({Colors.BLUE,?Colors.YELLOW})?@PathVariable?String?color)?{return?RestBody.okData(color); }上面兩種方式是不會生效的。不信你可以試一試,起碼在Spring Boot 2.3.1.RELEASE是不會直接生效的。
使以上兩種生效的方法是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行。這時候會拋出ConstraintViolationException異常。
集合類型參數中的元素不生效的問題
就像下面的寫法,方法的參數為集合時,如何檢驗元素的約束呢?
/***?集合類型參數元素.**?@param?student?the?student*?@return?the?rest*/ @PostMapping("/batchadd") public?Rest<?>?batchAddStudent(@Valid?@RequestBody?List<Student>?student)?{return?RestBody.okData(student); }同樣是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行。這時候會拋出ConstraintViolationException異常。
嵌套校驗不生效
嵌套的結構如何校驗呢?打個比方,如果我們在學生類Student中添加了其所屬的學校信息School并希望對School的屬性進行校驗。
@Data public?class?Student?{@NotBlank(message?=?"姓名必須填")private?String?name;@Ageprivate?Integer?age;@NotEmpty(message?=?"成績必填")private?List<Double>?scores;@NotNull(message?=?"學校不能為空")private?School?school; }@Data public?class?School?{@NotBlank(message?=?"學校名稱不能為空")private?String?name;@Min(value?=?0,message?="校齡大于0"?)private?Integer?age; }當 GET請求時正常校驗了School的屬性,但是POST請求卻無法對School的屬性進行校驗。這時我們只需要在該屬性上加上@Valid注解即可。
@Data public?class?Student?{@NotBlank(message?=?"姓名必須填")private?String?name;@Ageprivate?Integer?age;@NotEmpty(message?=?"成績必填")private?List<Double>?scores;@Valid@NotNull(message?=?"學校不能為空")private?School?school; }每加一層嵌套都需要加一層@Valid注解。通常在校驗對象屬性時,@NotNull、@NotEmpty和@Valid配合才能起到校驗效果。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的请不要再使用判断进行参数校验了的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 再有人问你MySql 的隔离级别是什么,
- 下一篇: 6993: Dominoes(纯bfs)