請(qǐng)求參數(shù)的校驗(yàn)是很多新手開發(fā)非常容易犯錯(cuò),或存在較多改進(jìn)點(diǎn)的常見場景。比較常見的問題主要表現(xiàn)在以下幾個(gè)方面:
- 僅依靠前端框架解決參數(shù)校驗(yàn),缺失服務(wù)端的校驗(yàn)。這種情況常見于需要同時(shí)開發(fā)前后端的時(shí)候,雖然程序的正常使用不會(huì)有問題,但是開發(fā)者忽略了非正常操作。比如繞過前端程序,直接模擬客戶端請(qǐng)求,這時(shí)候就會(huì)突然在前端預(yù)設(shè)的各種限制,直擊各種數(shù)據(jù)訪問接口,使得我們的系統(tǒng)存在安全隱患。
- 大量地使用
if/else
語句嵌套實(shí)現(xiàn),校驗(yàn)邏輯晦澀難通,不利于長期維護(hù)。
所以,針對(duì)上面的問題,建議服務(wù)端開發(fā)在實(shí)現(xiàn)接口的時(shí)候,對(duì)于請(qǐng)求參數(shù)必須要有服務(wù)端校驗(yàn)以保障數(shù)據(jù)安全與穩(wěn)定的系統(tǒng)運(yùn)行。同時(shí),對(duì)于參數(shù)的校驗(yàn)實(shí)現(xiàn)需要足夠優(yōu)雅,要滿足邏輯易讀、易維護(hù)的基本特點(diǎn)。
接下來,我們就來詳細(xì)說說,如何優(yōu)雅地實(shí)現(xiàn)Spring Boot服務(wù)端的請(qǐng)求參數(shù)校驗(yàn)。
JSR-303
在開始動(dòng)手實(shí)踐之前,我們先了解一下接下來我們將使用的一項(xiàng)標(biāo)準(zhǔn)規(guī)范:JSR-303
什么是JSR?
JSR是Java Specification Requests的縮寫,意思是Java 規(guī)范提案。是指向JCP(Java Community Process)提出新增一個(gè)標(biāo)準(zhǔn)化技術(shù)規(guī)范的正式請(qǐng)求。任何人都可以提交JSR,以向Java平臺(tái)增添新的API和服務(wù)。JSR已成為Java界的一個(gè)重要標(biāo)準(zhǔn)。
JSR-303定義的是什么標(biāo)準(zhǔn)?
JSR-303 是JAVA EE 6 中的一項(xiàng)子規(guī)范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的參考實(shí)現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實(shí)現(xiàn),除此之外還有一些附加的 constraint。
Bean Validation中內(nèi)置的constraint
Hibernate Validator附加的constraint
在JSR-303的標(biāo)準(zhǔn)之下,我們可以通過上面這些注解,定義各個(gè)請(qǐng)求參數(shù)的校驗(yàn)。
動(dòng)手實(shí)踐
已經(jīng)了解了JSR-303之后,接下來我們就來嘗試一下,基于此規(guī)范如何實(shí)現(xiàn)參數(shù)的校驗(yàn)!
準(zhǔn)備工作
可以拿任何一個(gè)使用Spring Boot 2.x構(gòu)建的提供RESTful API的項(xiàng)目作為基礎(chǔ)。
當(dāng)然,您也可以根據(jù)前文再構(gòu)建一個(gè)作為復(fù)習(xí),也是完全沒有問題的。
快速入門
我們先來做一個(gè)簡單的例子,比如:定義字段不能為Null
。只需要兩步
第一步:在要校驗(yàn)的字段上添加上@NotNull
注解,具體如下:
@Data
@ApiModel(description="用戶實(shí)體")
public class User {
@ApiModelProperty("用戶編號(hào)")
private Long id;
@NotNull
@ApiModelProperty("用戶姓名")
private String name;
@NotNull
@ApiModelProperty("用戶年齡")
private Integer age;
}
第二步:在需要校驗(yàn)的參數(shù)實(shí)體前添加@Valid注解,具體如下:
@PostMapping("/")
@ApiOperation(value = "創(chuàng)建用戶", notes = "根據(jù)User對(duì)象創(chuàng)建用戶")
public String postUser(@Valid @RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
完成上面配置之后,啟動(dòng)應(yīng)用,并用POST請(qǐng)求訪問localhost:8080/users/接口,body使用一個(gè)空對(duì)象,{}。你可以用Postman等測試工具發(fā)起,也可以使用curl發(fā)起,比如這樣:
curl -X POST \
http://localhost:8080/users/ \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 72745d04-caa5-44a1-be84-ba9c115f4dfb' \
-H 'cache-control: no-cache' \
-d '{
}'
不出意外,你可以得到如下結(jié)果:
{
"timestamp": "2019-10-05T05:45:19.221+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.user.age",
"NotNull.age",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
}
],
"defaultMessage": "不能為null",
"objectName": "user",
"field": "age",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.user.name",
"NotNull.name",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能為null",
"objectName": "user",
"field": "name",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='user'. Error count: 2",
"path": "/users/"
}
其中返回內(nèi)容的各參數(shù)含義如下:
timestamp
:請(qǐng)求時(shí)間status
:HTTP返回的狀態(tài)碼,這里返回400,即:請(qǐng)求無效、錯(cuò)誤的請(qǐng)求,通常參數(shù)校驗(yàn)不通過均為400error
:HTTP返回的錯(cuò)誤描述,這里對(duì)應(yīng)的就是400狀態(tài)的錯(cuò)誤描述:Bad Requesterrors
:具體錯(cuò)誤原因,是一個(gè)數(shù)組類型;因?yàn)殄e(cuò)誤校驗(yàn)可能存在多個(gè)字段的錯(cuò)誤,比如這里因?yàn)槎x了兩個(gè)參數(shù)不能為Null
,所以存在兩條錯(cuò)誤記錄信息message
:概要錯(cuò)誤消息,返回內(nèi)容中很容易可以知道,這里的錯(cuò)誤原因是對(duì)user對(duì)象的校驗(yàn)失敗,其中錯(cuò)誤數(shù)量為2
,而具體的錯(cuò)誤信息就定義在上面的errors
數(shù)組中path
:請(qǐng)求路徑
請(qǐng)求的調(diào)用端在拿到這個(gè)規(guī)范化的錯(cuò)誤信息之后,就可以方便的解析并作出對(duì)應(yīng)的措施以完成自己的業(yè)務(wù)邏輯了。
嘗試一些其他校驗(yàn)
在完成了上面的例子之后,我們還可以增加一些校驗(yàn)規(guī)則,比如:校驗(yàn)字符串的長度、校驗(yàn)數(shù)字的大小、校驗(yàn)字符串格式是否為郵箱等。下面我們就來定義一些復(fù)雜的校驗(yàn)定義,比如:
@Data
@ApiModel(description="用戶實(shí)體")
public class User {
@ApiModelProperty("用戶編號(hào)")
private Long id;
@NotNull
@Size(min = 2, max = 5)
@ApiModelProperty("用戶姓名")
private String name;
@NotNull
@Max(100)
@Min(10)
@ApiModelProperty("用戶年齡")
private Integer age;
@NotNull
@Email
@ApiModelProperty("用戶郵箱")
private String email;
}
發(fā)起一個(gè)可以出發(fā)name、age、email都校驗(yàn)不通過的請(qǐng)求,比如下面這樣:
curl -X POST \
http://localhost:8080/users/ \
-H 'Content-Type: application/json' \
-H 'Postman-Token: 114db0f0-bdce-4ba5-baf6-01e5104a68a3' \
-H 'cache-control: no-cache' \
-d '{
"name": "abcdefg",
"age": 8,
"email": "aaaa"
}'
我們將得到如下的錯(cuò)誤返回:
{
"timestamp": "2019-10-05T06:24:30.518+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Size.user.name",
"Size.name",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"user.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
},
5,
2
],
"defaultMessage": "個(gè)數(shù)必須在2和5之間",
"objectName": "user",
"field": "name",
"rejectedValue": "abcdefg",
"bindingFailure": false,
"code": "Size"
},
{
"codes": [
"Min.user.age",
"Min.age",
"Min.java.lang.Integer",
"Min"
],
"arguments": [
{
"codes": [
"user.age",
"age"
],
"arguments": null,
"defaultMessage": "age",
"code": "age"
},
10
],
"defaultMessage": "最小不能小于10",
"objectName": "user",
"field": "age",
"rejectedValue": 8,
"bindingFailure": false,
"code": "Min"
},
{
"codes": [
"Email.user.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"user.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"defaultMessage": ".*",
"codes": [
".*"
],
"arguments": null
}
],
"defaultMessage": "不是一個(gè)合法的電子郵件地址",
"objectName": "user",
"field": "email",
"rejectedValue": "aaaa",
"bindingFailure": false,
"code": "Email"
}
],
"message": "Validation failed for object='user'. Error count: 3",
"path": "/users/"
}
從errors
數(shù)組中的各個(gè)錯(cuò)誤明細(xì)中,知道各個(gè)字段的defaultMessage
,可以看到很清晰的錯(cuò)誤描述。
Swagger文檔中的體現(xiàn)
可能有朋友會(huì)問了,我的接口中是定了這么多。上一篇教程中,不是還教了如何自動(dòng)生成文檔么,那么對(duì)于參數(shù)的校驗(yàn)邏輯該如何描述呢?
這里要分兩種情況,Swagger自身對(duì)JSR-303有一定的支持,但是支持的并那么完善,并沒有覆蓋所有的注解的。
比如,上面我們使用的注解是可以自動(dòng)生成的,啟動(dòng)上面我們的實(shí)驗(yàn)工程,然后訪問http://localhost:8080/swagger-ui.html
,在Models
不是,我們可以看到如下圖所示的內(nèi)容:
其中:name和age字段相比上一篇教程中的文檔描述,多了一些關(guān)于校驗(yàn)相關(guān)的說明;而email字段則沒有體現(xiàn)相關(guān)校驗(yàn)說明。目前,Swagger共支持以下幾個(gè)注解:@NotNull、@Max、@Min、@Size、@Pattern。在實(shí)際開發(fā)過程中,我們需要分情況來處理,對(duì)于Swagger支自動(dòng)生成的可以利用原生支持來產(chǎn)生,如果有部分字段無法產(chǎn)生,則可以在@ApiModelProperty注解的描述中他,添加相應(yīng)的校驗(yàn)說明,以便于使用方查看。
番外:也許你會(huì)有這些疑問
當(dāng)請(qǐng)求參數(shù)校驗(yàn)出現(xiàn)錯(cuò)誤信息的時(shí)候,錯(cuò)誤格式可以修改嗎?
答案是肯定的。這里的錯(cuò)誤信息實(shí)際上由Spring Boot的異常處理機(jī)制統(tǒng)一組織并返回的,我們將在后面的教程中詳細(xì)介紹,Spring Boot是如何統(tǒng)一處理異常返回以及我們?cè)撊绾味〞r(shí)異常返回。
spring-boot-starter-validation是必須的嗎?
有讀者之前問過,看到很多教程都寫了還要引入spring-boot-starter-validation
依賴,這個(gè)依賴到底是否需要?(本篇中并沒有引入)
org.springframework.boot
spring-boot-starter-validation
其實(shí),只需要仔細(xì)看一下spring-boot-starter-validation依賴主要是為了引入了什么,再根據(jù)當(dāng)前自己使用的Spring Boot版本來判斷即可。實(shí)際上,spring-boot-starter-validation依賴主要是為了引入下面這個(gè)依賴:
org.hibernate.validator
hibernate-validator
6.0.14.Final
compile
我們可以看看當(dāng)前工程的依賴中是否有它,就可以判斷是否還需要額外引入。在Spring Boot 2.1版本中,該依然其實(shí)已經(jīng)包含在了spring-boot-starter-web依賴中,并不需要額外引入,所以您在本文中找不到這一步。
注:本文轉(zhuǎn)載自“程序猿DD”,如有侵權(quán),請(qǐng)聯(lián)系刪除!