請求參數的校驗是很多新手開發非常容易犯錯,或存在較多改進點的常見場景。比較常見的問題主要表現在以下幾個方面:
- 僅依靠前端架構解決參數校驗,缺失服務端的校驗。這種情況常見于需要同時開發前後端的時候,雖然程式的正常使用不會有問題,但是開發者忽略了非正常操作。比如繞過前端程式,直接模拟用戶端請求,這時候就會突然在前端預設的各種限制,直擊各種資料通路接口,使得我們的系統存在安全隐患。
- 大量地使用
語句嵌套實作,校驗邏輯晦澀難通,不利于長期維護。if/else
是以,針對上面的問題,建議服務端開發在實作接口的時候,對于請求參數必須要有服務端校驗以保障資料安全與穩定的系統運作。同時,對于參數的校驗實作需要足夠優雅,要滿足邏輯易讀、易維護的基本特點。
接下來,我們就在本篇教程中詳細說說,如何優雅地實作Spring Boot服務端的請求參數校驗。
JSR-303
在開始動手實踐之前,我們先了解一下接下來我們将使用的一項标準規範:JSR-303
什麼是JSR?
JSR是Java Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process)提出新增一個标準化技術規範的正式請求。任何人都可以送出JSR,以向Java平台增添新的API和服務。JSR已成為Java界的一個重要标準。
JSR-303定義的是什麼标準?
JSR-303 是JAVA EE 6 中的一項子規範,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的參考實作 . Hibernate Validator 提供了 JSR 303 規範中所有内置 constraint 的實作,除此之外還有一些附加的 constraint。
Bean Validation中内置的constraint
Hibernate Validator附加的constraint
在JSR-303的标準之下,我們可以通過上面這些注解,優雅的定義各個請求參數的校驗。更多關于JSR的内容可以參與官方文檔或參考資料中的引文[1]。
動手實踐
已經了解了JSR-303之後,接下來我們就來嘗試一下,基于此規範如何實作參數的校驗!
準備工作
讀者可以拿任何一個使用Spring Boot 2.x建構的提供RESTful API的項目作為基礎。也可以使用
Spring Boot 2.x基礎教程:使用Swagger2建構強大的API文檔中建構的實驗工程作為基礎,您可以通過下面倉庫中的
chapter2-2
目錄取得:
- Github: https://github.com/dyc87112/SpringBoot-Learning/tree/2.x
- Gitee: https://gitee.com/didispace/SpringBoot-Learning/tree/2.x
當然,您也可以根據前文再建構一個作為複習,也是完全沒有問題的。
快速入門
我們先來做一個簡單的例子,比如:定義字段不能為
Null
。隻需要兩步
第一步:在要校驗的字段上添加上
@NotNull
注解,具體如下:
@Data
@ApiModel(description="使用者實體")
public class User {
@ApiModelProperty("使用者編号")
private Long id;
@NotNull
@ApiModelProperty("使用者姓名")
private String name;
@NotNull
@ApiModelProperty("使用者年齡")
private Integer age;
}
第二步:在需要校驗的參數實體前添加
@Valid
@PostMapping("/")
@ApiOperation(value = "建立使用者", notes = "根據User對象建立使用者")
public String postUser(@Valid @RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
完成上面配置之後,啟動應用,并用POST請求通路
localhost:8080/users/
接口,body使用一個空對象,
{}
。你可以用Postman等測試工具發起,也可以使用curl發起,比如這樣:
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 '{
}'
不出意外,你可以得到如下結果:
{
"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/"
}
其中傳回内容的各參數含義如下:
-
:請求時間timestamp
-
:HTTP傳回的狀态碼,這裡傳回400,即:請求無效、錯誤的請求,通常參數校驗不通過均為400status
-
:HTTP傳回的錯誤描述,這裡對應的就是400狀态的錯誤描述:Bad Requesterror
-
:具體錯誤原因,是一個數組類型;因為錯誤校驗可能存在多個字段的錯誤,比如這裡因為定義了兩個參數不能為errors
,是以存在兩條錯誤記錄資訊Null
-
:概要錯誤消息,傳回内容中很容易可以知道,這裡的錯誤原因是對user對象的校驗失敗,其中錯誤數量為message
,而具體的錯誤資訊就定義在上面的2
數組中errors
-
:請求路徑path
請求的調用端在拿到這個規範化的錯誤資訊之後,就可以友善的解析并作出對應的措施以完成自己的業務邏輯了。
嘗試一些其他校驗
在完成了上面的例子之後,我們還可以增加一些校驗規則,比如:校驗字元串的長度、校驗數字的大小、校驗字元串格式是否為郵箱等。下面我們就來定義一些複雜的校驗定義,比如:
@Data
@ApiModel(description="使用者實體")
public class User {
@ApiModelProperty("使用者編号")
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;
}
發起一個可以出發
name
、
age
email
都校驗不通過的請求,比如下面這樣:
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"
}'
我們将得到如下的錯誤傳回:
{
"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": "個數必須在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": "不是一個合法的電子郵件位址",
"objectName": "user",
"field": "email",
"rejectedValue": "aaaa",
"bindingFailure": false,
"code": "Email"
}
],
"message": "Validation failed for object='user'. Error count: 3",
"path": "/users/"
}
從
errors
數組中的各個錯誤明細中,知道各個字段的
defaultMessage
,可以看到很清晰的錯誤描述。
Swagger文檔中的展現
可能有讀者會問了,我的接口中是定了這麼多。上一篇教程中,不是還教了如何自動生成文檔麼,那麼對于參數的校驗邏輯該如何描述呢?
這裡要分兩種情況,Swagger自身對JSR-303有一定的支援,但是支援的并那麼完善,并沒有覆寫所有的注解的。
比如,上面我們使用的注解是可以自動生成的,啟動上面我們的實驗工程,然後通路
http://localhost:8080/swagger-ui.html
,在
Models
不是,我們可以看到如下圖所示的内容:
其中:
name
和
age
字段相比上一篇教程中的文檔描述,多了一些關于校驗相關的說明;而
email
字段則沒有展現相關校驗說明。目前,Swagger共支援以下幾個注解:
@NotNull
@Max
@Min
@Size
@Pattern
。在實際開發過程中,我們需要分情況來處理,對于Swagger支自動生成的可以利用原生支援來産生,如果有部分字段無法産生,則可以在
@ApiModelProperty
注解的描述中他,添加相應的校驗說明,以便于使用方檢視。
番外:也許你會有這些疑問
當請求參數校驗出現錯誤資訊的時候,錯誤格式可以修改嗎?
答案是肯定的。這裡的錯誤資訊實際上由Spring Boot的異常處理機制統一組織并傳回的,我們将在後面的教程中詳細介紹,Spring Boot是如何統一處理異常傳回以及我們該如何定時異常傳回。
spring-boot-starter-validation
是必須的嗎?
有讀者之前問過,看到很多教程都寫了還要引入
spring-boot-starter-validation
依賴,這個依賴到底是否需要?(本篇中并沒有引入)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
其實,隻需要仔細看一下
spring-boot-starter-validation
依賴主要是為了引入了什麼,再根據目前自己使用的Spring Boot版本來判斷即可。實際上,
spring-boot-starter-validation
依賴主要是為了引入下面這個依賴:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.14.Final</version>
<scope>compile</scope>
</dependency>
我們可以看看目前工程的依賴中是否有它,就可以判斷是否還需要額外引入。在Spring Boot 2.1版本中,該依然其實已經包含在了
spring-boot-starter-web
依賴中,并不需要額外引入,是以您在本文中找不到這一步。
代碼示例
本文的完整工程可以檢視下面倉庫中的
chapter2-3
目錄:
如果您覺得本文不錯,歡迎Star支援,您的關注是我堅持的動力!