Spring Boot 2.x基礎教程:JSR-303實現請求參數校驗

  • 2019 年 10 月 8 日
  • 筆記

請求參數的校驗是很多新手開發非常容易犯錯,或存在較多改進點的常見場景。比較常見的問題主要表現在以下幾個方面:

  • 僅依靠前端框架解決參數校驗,缺失服務端的校驗。這種情況常見於需要同時開發前後端的時候,雖然程序的正常使用不會有問題,但是開發者忽略了非正常操作。比如繞過前端程序,直接模擬客戶端請求,這時候就會突然在前端預設的各種限制,直擊各種數據訪問接口,使得我們的系統存在安全隱患。
  • 大量地使用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目錄取得:

當然,您也可以根據前文再構建一個作為複習,也是完全沒有問題的。

快速入門

我們先來做一個簡單的例子,比如:定義字段不能為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:請求時間
  • status:HTTP返回的狀態碼,這裡返回400,即:請求無效、錯誤的請求,通常參數校驗不通過均為400
  • error:HTTP返回的錯誤描述,這裡對應的就是400狀態的錯誤描述:Bad Request
  • errors:具體錯誤原因,是一個數組類型;因為錯誤校驗可能存在多個字段的錯誤,比如這裡因為定義了兩個參數不能為Null,所以存在兩條錯誤記錄信息
  • message:概要錯誤消息,返回內容中很容易可以知道,這裡的錯誤原因是對user對象的校驗失敗,其中錯誤數量為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;    }

發起一個可以出發nameageemail都校驗不通過的請求,比如下面這樣:

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不是,我們可以看到如下圖所示的內容:

其中:nameage字段相比上一篇教程中的文檔描述,多了一些關於校驗相關的說明;而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支持,您的關注是我堅持的動力!

參考資料

  • [1] https://www.jianshu.com/p/554533f88370
  • [2] http://blog.didispace.com/spring-boot-starter-swagger-1.3.0/

歡迎關注我的公眾號:程序猿DD,獲得獨家整理的學習資源和日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:didispace.com