請(qǐng)不要再使用判斷進(jìn)行參數(shù)校驗(yàn)了
1. 前言
因?yàn)榫W(wǎng)絡(luò)傳輸?shù)牟豢煽啃裕约扒岸藬?shù)據(jù)控制的可篡改性,后端的參數(shù)校驗(yàn)是必須的,應(yīng)用程序必須通過(guò)某種手段來(lái)確保輸入進(jìn)來(lái)的數(shù)據(jù)從語(yǔ)義上來(lái)講是正確的。2. 數(shù)據(jù)校驗(yàn)的痛點(diǎn)
為了保證數(shù)據(jù)語(yǔ)義的正確,我們需要進(jìn)行大量的判斷來(lái)處理驗(yàn)證邏輯。而且項(xiàng)目的分層也會(huì)造成一些重復(fù)的校驗(yàn),產(chǎn)生大量與業(yè)務(wù)無(wú)關(guān)的代碼。不利于代碼的維護(hù),增加了開(kāi)發(fā)人員的工作量。3. JSR 303 校驗(yàn)規(guī)范及其實(shí)現(xiàn)
為了解決上面的痛點(diǎn),將驗(yàn)證邏輯與相應(yīng)的領(lǐng)域模型進(jìn)行綁定是十分有必要的。為此產(chǎn)生了JSR 303 – Bean Validation 規(guī)范。Hibernate Validator 是JSR-303的參考實(shí)現(xiàn),它提供了JSR 303規(guī)范中所有的約束(constraint)的實(shí)現(xiàn),同時(shí)也增加了一些擴(kuò)展。Hibernate Validator 提供的常用約束注解
| 約束注解 | 詳細(xì)信息 |
|---|---|
@Null | 被注釋的元素必須為 null |
@NotNull | 被注釋的元素必須不為 null |
@AssertTrue | 被注釋的元素必須為 true |
@AssertFalse | 被注釋的元素必須為 false |
@Min(value) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于等于指定的最小值 |
@Max(value) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于等于指定的最大值 |
@DecimalMin(value) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于等于指定的最小值 |
@DecimalMax(value) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須小于等于指定的最大值 |
@Size(max, min) | 被注釋的元素的大小必須在指定的范圍內(nèi) |
@Digits (integer, fraction) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須在可接受的范圍內(nèi) |
@Past | 被注釋的元素必須是一個(gè)過(guò)去的日期 |
@Future | 被注釋的元素必須是一個(gè)將來(lái)的日期 |
@Pattern(value) | 被注釋的元素必須符合指定的正則表達(dá)式 |
@Email | 被注釋的元素必須是電子郵箱地址 |
@Length | 被注釋的字符串的大小必須在指定的范圍內(nèi) |
@NotEmpty | 被注釋的字符串的必須非空 |
@Range | 被注釋的元素必須在合適的范圍內(nèi) |
4. 驗(yàn)證注解的使用
在Spring Boot開(kāi)發(fā)中使用Hibernate Validator是非常容易的,引入下面的starter就可以了:
<dependency>
????<groupId>org.springframework.bootgroupId>
????<artifactId>spring-boot-starter-validationartifactId>
dependency>
一種可以實(shí)現(xiàn)接口來(lái)定制Validator,一種是使用約束注解。胖哥覺(jué)得注解可以滿足絕大部分的需求,所以建議使用注解來(lái)進(jìn)行數(shù)據(jù)校驗(yàn)。而且注解更加靈活,控制的粒度也更加細(xì)。接下來(lái)我們來(lái)學(xué)習(xí)如何使用注解進(jìn)行數(shù)據(jù)校驗(yàn)。
4.1 約束注解的基本使用
我們對(duì)需要校驗(yàn)的方法入?yún)⑦M(jìn)行注解約束標(biāo)記,例子如下:
@Data
public?class?Student?{
????@NotBlank(message?=?"姓名必須填")
????private?String?name;
????@NotNull(message?=?"年齡必須填寫")
????@Range(min?=?1,max?=50,?message?=?"年齡取值范圍1-50")
????private?Integer?age;
????@NotEmpty(message?=?"成績(jī)必填")
????private?List?scores;
}
POST 請(qǐng)求
然后定義一個(gè)POST請(qǐng)求的Spring MVC接口:
@RestController
@RequestMapping("/student")
public?class?StudentController?{
????@PostMapping("/add")
????public?Rest>?addStudent(@Valid?@RequestBody?Student?student)?{
????????return?RestBody.okData(student);
????}
}
通過(guò)對(duì)addStudent方法入?yún)⑻砑?/span>@Valid來(lái)啟用參數(shù)校驗(yàn)。當(dāng)使用下面數(shù)據(jù)進(jìn)行請(qǐng)求將會(huì)拋出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 請(qǐng)求
如法炮制,我們定義一個(gè)GET請(qǐng)求的接口:
@GetMapping("/get")
public?Rest>?getStudent(@Valid?Student?student)?{
????return?RestBody.okData(student);
}
使用下面的請(qǐng)求可以正確對(duì)學(xué)生分?jǐn)?shù)scores進(jìn)行了校驗(yàn),但是拋出的并不是MethodArgumentNotValidException異常,而是BindException異常。這和使用@RequestBody注解有關(guān)系,這對(duì)我們后面的統(tǒng)一處理非常十分重要。
GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888
自定義注解
可能有些同學(xué)注意到上面的年齡我進(jìn)行了這樣的標(biāo)記:
@NotNull(message?=?"年齡必須填寫")
@Range(min?=?1,max?=50,?message?=?"年齡取值范圍1-50")
private?Integer?age;
這是因?yàn)?/span>@Range不會(huì)去校驗(yàn)為空的情況,它只處理非空的時(shí)候是否符合范圍約束。所以要用多個(gè)注解來(lái)約束。如果我們某些場(chǎng)景需要重復(fù)的捆綁多個(gè)注解來(lái)使用時(shí),可以使用自定義注解將它們封裝起來(lái)組合使用,下面這個(gè)注解就是將@NotNull和@Range進(jìn)行了組合,你可以仿一個(gè)出來(lái)用用看。
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?{};
}
還有一種情況,我們?cè)诤笈_(tái)定義了枚舉值來(lái)進(jìn)行狀態(tài)的流轉(zhuǎn),也是需要校驗(yàn)的,比如我們定義了顏色枚舉:
public?enum?Colors?{
????RED,?YELLOW,?BLUE
}
我們希望入?yún)⒉荒艹?/span>Colors的范圍["RED", "YELLOW", "BLUE"],這就需要實(shí)現(xiàn)ConstraintValidator接口來(lái)定義一個(gè)顏色約束了,其中泛型A為自定義的約束注解,泛型T為入?yún)⒌念愋停@里使用字符串,然后我們的實(shí)現(xiàn)如下:
/**
?*?@author?felord.cn
?*?@since?17:57
?**/
public?class?ColorConstraintValidator?implements?ConstraintValidator<Color,?String>?{
????private?static?final?Set?COLOR_CONSTRAINTS?=?new?HashSet<>();
????@Override
????public?void?initialize(Color?constraintAnnotation)?{
????????Colors[]?value?=?constraintAnnotation.value();
????????List?list?=?Arrays.stream(value)
????????????????.map(Enum::name)
????????????????.collect(Collectors.toList());
????????COLOR_CONSTRAINTS.addAll(list);
????}
????@Override
????public?boolean?isValid(String?value,?ConstraintValidatorContext?context)?{
????????return?COLOR_CONSTRAINTS.contains(value);
????}
}
然后聲明對(duì)應(yīng)的約束注解Color,需要在元注解@Constraint中指明使用上面定義好的處理類ColorConstraintValidator進(jìn)行校驗(yàn)。/**
?*?@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?{
????//?錯(cuò)誤提示信息
????String?message()?default?"顏色不符合規(guī)格";
????Class>[]?groups()?default?{};
????Class?extends?Payload>[]?payload()?default?{};
????//?約束的類型
????Colors[]?value();
}
然后我們來(lái)試一下,先對(duì)參數(shù)進(jìn)行約束:
@Data
public?class?Param?{
????@Color({Colors.BLUE,Colors.YELLOW})
???private?String?color;
}
接口跟上面幾個(gè)一樣,調(diào)用下面的接口將拋出BindException異常:
GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888
當(dāng)我們把參數(shù)color賦值為BLUE或者YELLOW后,能夠成功得到響應(yīng)。
4.2 常見(jiàn)問(wèn)題
在實(shí)際使用起來(lái)我們會(huì)遇到一些問(wèn)題,這里總結(jié)了一些常見(jiàn)的問(wèn)題和處理方式。
檢驗(yàn)基礎(chǔ)類型不生效的問(wèn)題
上面為了校驗(yàn)顏色我們聲明了一個(gè)Param對(duì)象來(lái)包裝唯一的字符串參數(shù)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);
}
上面兩種方式是不會(huì)生效的。不信你可以試一試,起碼在Spring Boot 2.3.1.RELEASE是不會(huì)直接生效的。
使以上兩種生效的方法是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行。這時(shí)候會(huì)拋出ConstraintViolationException異常。
集合類型參數(shù)中的元素不生效的問(wèn)題
就像下面的寫法,方法的參數(shù)為集合時(shí),如何檢驗(yàn)元素的約束呢?
/**
?*?集合類型參數(shù)元素.
?*
?*?@param?student?the?student
?*?@return?the?rest
?*/
@PostMapping("/batchadd")
public?Rest>?batchAddStudent(@Valid?@RequestBody?List?student)?{
????return?RestBody.okData(student);
}
同樣是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行。這時(shí)候會(huì)拋出ConstraintViolationException異常。
嵌套校驗(yàn)不生效
嵌套的結(jié)構(gòu)如何校驗(yàn)?zāi)兀看騻€(gè)比方,如果我們?cè)趯W(xué)生類Student中添加了其所屬的學(xué)校信息School并希望對(duì)School的屬性進(jìn)行校驗(yàn)。
@Data
public?class?Student?{
????@NotBlank(message?=?"姓名必須填")
????private?String?name;
????@Age
????private?Integer?age;
????@NotEmpty(message?=?"成績(jī)必填")
????private?List?scores;
????@NotNull(message?=?"學(xué)校不能為空")
????private?School?school;
}
@Data
public?class?School?{
????@NotBlank(message?=?"學(xué)校名稱不能為空")
????private?String?name;
????@Min(value?=?0,message?="校齡大于0"?)
????private?Integer?age;
}
當(dāng) GET請(qǐng)求時(shí)正常校驗(yàn)了School的屬性,但是POST請(qǐng)求卻無(wú)法對(duì)School的屬性進(jìn)行校驗(yàn)。這時(shí)我們只需要在該屬性上加上@Valid注解即可。
@Data
public?class?Student?{
????@NotBlank(message?=?"姓名必須填")
????private?String?name;
????@Age
????private?Integer?age;
????@NotEmpty(message?=?"成績(jī)必填")
????private?List?scores;
????@Valid
????@NotNull(message?=?"學(xué)校不能為空")
????private?School?school;
}
每加一層嵌套都需要加一層
@Valid注解。通常在校驗(yàn)對(duì)象屬性時(shí),@NotNull、@NotEmpty和@Valid配合才能起到校驗(yàn)效果。
有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
