前言
作為服務端開發,驗證前端傳入的引數的合法性是一個必不可少的步驟,但是驗證引數基本上是一個體力活,而且冗余代碼繁多,也影響代碼的可閱讀性,所以有沒有一個比較優雅的方式來解決這個問題?
JSR-303驗證框架,JSR-303 是Java EE 6 中的一項子規范,叫做Bean Validation,官方參考實作是Hibernate Validator(與Hibernate ORM 沒有關系),JSR 303 用于對Java Bean 中的欄位的值進行驗證,確保輸入進來的資料在語意上是正確的,使驗證邏輯從業務代碼中脫離出來,JSR303是運行時資料驗證框架,驗證之后驗證的錯誤資訊會馬上回傳,
依賴
由于 spring-boot-starter-web 中提供的引數校驗注解較少(如下圖所示)

所以可以引入 spring-boot-starter-validation 來豐富專案中可以參考的引數校驗注解

校驗組
校驗組可以對需要校驗的欄位進行分類,即為每個欄位提供不同的校驗規則,校驗組只需定義簡單的介面即可,本文案例中我們定義兩個校驗組 ReadAction 和 WriteAction,分別對應讀和寫兩種情況下的欄位的校驗規則,
public interface ReadAction {
}
public interface WriteAction {
}
舉例說明其使用方式,比如一個欄位 id,只需要在寫操作時前端傳參不為 null,那么可以在 id 上加注解:
@NotNull(groups = {WriteAction.class})
private String id;
如果需要在兩種情況下都不為 null,那么:
@NotNull(groups = {ReadWriteAction.class, WriteAction.class})
private String id;
如果需要在讀時不為空,寫時不為 null,那么可以加上以下兩個注解,對應不同的組:
@NotBlank(groups = {ReadWriteAction.class})
@NotNull(groups = {WriteAction.class})
private String id;
需要注意的是只有在 Bean 引數前使用 @Validated(value = {Group.class...}) 且必須在 value 中寫明 groups 對應的類物件時 groups 屬性才會生效,
若是以下這種沒有 groups 屬性的形式,那么需要使用 @Valid 注解才能使之生效
@NotBlank
@NotNull
private String id;
物體類
首先定義一個物體類 PersonReq
package com.jake.spring.boot.validation.model;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
@Data
public class PersonReq {
@Valid
@NotNull(groups = {WriteAction.class, ReadAction.class})
private IdCardInfo idCardInfo;
@PositiveOrZero
@NotNull
private Integer age;
@Email(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String email;
@Length(min = 11, max = 11, groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String phone;
}
其中,WriteAction 和 ReadAction 是之前自定義的分組標志介面,
根據校驗注解名稱,可以很清晰地了解這些注解的作用(比如 @PositiveOrZero 表示年齡需要是一個大于等于 0 的數字),此處不詳細敘述,
而 @Valid 能夠進行嵌套校驗,即對于 Bean 中的另一個 Bean 屬性做引數校驗,
接著,定義其中的 Bean 屬性 IdCardInfo
package com.jake.spring.boot.validation.model;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.time.LocalDate;
@Data
public class IdCardInfo {
@Length(min = 18, max = 18, groups = {WriteAction.class})
@NotNull(groups = {WriteAction.class})
private String id;
@NotBlank(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String name;
@NotBlank(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String address;
@Past(groups = {ReadAction.class})
@DateTimeFormat(pattern = "YYYY-MM-dd")
@NotNull(groups = {ReadAction.class})
private LocalDate birthday;
@Future(groups = {ReadAction.class})
@DateTimeFormat(pattern = "YYYY-MM-dd")
@NotNull(groups = {ReadAction.class})
private LocalDate expiration;
}
控制層
package com.jake.spring.boot.validation.controller;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import com.jake.spring.boot.validation.model.PersonReq;
import com.jake.spring.boot.validation.model.PersonVO;
import org.hibernate.validator.constraints.Length;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("persons")
@Validated
public class PersonController {
@PostMapping("read")
public PersonVO read(@RequestBody @Validated(value = {ReadAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("write")
public PersonVO write(@RequestBody @Validated(value = {WriteAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("readOrWrite")
public PersonVO readOrWrite(@RequestBody @Validated(value = {ReadAction.class, WriteAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("copy")
public PersonVO copy(@RequestBody @Valid PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@GetMapping("{idNo}")
public String id(@Length(min = 18, max = 18) @PathVariable(name = "idNo") String id) {
return id;
}
}
注意,對于有 groups 屬性定義的欄位,必須使用 @Validated(value = {Group.class...}) 其校驗注解才能生效;對于沒有 groups 屬性定義的欄位,則必須使用 @Valid,
對于各個介面,其對應的呼叫引數如下:
-
read(針對分組屬性為
ReadAction)curl --location --request POST 'localhost:8080/persons/read' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "name": "wzk", "address": "sz", "birthday": "1992-06-01", "expiration": "2027-07-07" }, "age": 28, "email": "15118126432@163.com", "phone": "15118126432" }' -
write(針對分組屬性為
WriteAction)curl --location --request POST 'localhost:8080/persons/write' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "id": "44528119920601005X" } }' -
readOrWrite(針對分組屬性為
ReadAction或WriteAction)curl --location --request POST 'localhost:8080/persons/readOrWrite' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "id": "44528119920601005X", "name": "wzk", "address": "sz", "birthday": "1992-06-01", "expiration": "2027-07-07" }, "email": "15118126432@163.com", "phone": "15118126432" }' -
copy(針對無分組屬性的情況,在本文代碼案例中僅針對
PersonReq的 age 屬性)curl --location --request POST 'localhost:8080/persons/copy' \ --header 'Content-Type: application/json' \ --data-raw '{ "age": 28 }'
以上四組介面呼叫腳本均不會出現由引數格式錯誤引起的 400 Bad Request 錯誤,而且是請求引數最少化的形式,
例如,在調 read 介面時,無需填入 id,因為屬性 id 的注解的分組屬性中沒有 ReadAction;對于 write 介面同理,只需填入欄位 idCardInfo 及id,而其他屬性均沒有被 WriteAction 定義;在調 readOrWrite 時,則除了沒有定義 groups 屬性的 age 之外,其他含有 ReadAction 和 WriteAction 的屬性都要帶上;而對于 copy 介面,則只需要關注沒有 groups 屬性的 age 欄位,
另外,在 PersonController 中有一個 id 方法是直接在介面入參處使用 @Length 注解做引數校驗的,但要使該注解生效,需要在 PersonController 上加上 @Validated 注解,
參考博客
SpringBoot中的引數校驗
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/232552.html
標籤:java
