寫在前面
上一篇文章中我們學會了如何優雅的接收前端引數,傳送門
SpringBoot如何優雅的接收前端引數
接收到引數後,接下來要做的就是校驗引數的合法性。這一步的重要性就不用多說了。
即使前端已經對資料進行了校驗,我們後端還是要再對接收到的資料進行一遍徹底的校驗。
這樣可以避免張三等人利用Http工具,繞過瀏覽器非法請求資料。
廢話不多說,看完這篇文章,你將從繁瑣的校驗邏輯中解脫出來
一、傳統引數校驗
雖然往事不堪回首,但還是得回憶一下我們傳統引數校驗的痛點。
下面是我們傳統校驗使用者名稱和郵箱是否合法的程式碼
if (username == null || username.isEmpty()) {
throw new IllegalArgumentException("使用者名稱不能為空");
}
if (isValidEmail(email)) {
throw new IllegalArgumentException("郵箱格式不正確");
}
public boolean isValidEmail(String email) {
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
Pattern pattern = Pattern.compile(emailRegex);
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
這樣的程式碼不僅冗長,而且難以維護,尤其是在多個地方重複使用時,容易出錯。
面對上面的痛點,我們就得解放雙手,利用框架來完成校驗。
它只需要透過簡單的註解來定義校驗規則,讓框架來幫助我們處理校驗邏輯,讓我們程式碼變得更加的優雅。
二、幾個名詞
問題①:JSR是什麼?
JSR(Java Specification Requests) 是一套 JavaBean
引數校驗的標準,它定義了很多常用的校驗註解。
我們可以直接將這些註解加在我們 JavaBean
的屬性上面,這樣就可以在需要校驗的時候進行校驗了,非常方便!
問題②:Bean Validation是什麼?
Bean Validation
是一個抽象的框架,它定義了驗證規則,而不會涉及具體的業務邏輯
問題③:Hibernate Validator是什麼?
是Bean Validation
的實現,目前最新版的 Hibernate Validator 6.x
是 Bean Validation 2.0(JSR 380)
的參考實現
三、所需依賴
Spring boot 2.3以前版本,
Springboot
的spring-boot-starter-web
預設內建了Hibernate-Validator
這些版本直接引入
spring-boot-starter-web
即可,後面的版本需要單獨引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
在後面的測試中會用到lombok
、SpringBoot
的web
、test
等基礎依賴,這裡就不一一給出
四、註解及作用
註解 | 作用型別 | 作用 |
---|---|---|
@NotBlank(message='') |
字串 | 被註釋字串非null,且長度必須大於0 |
@NotEmpty |
字串 | 被註釋的字串必須非空 |
@NotNull |
任意 | 被註釋的元素不能為null |
@Null |
任意 | 被註釋的元素必須為null |
@Email |
字串 | 被註釋的元素必須是電子郵箱地址 |
@AssertTrue |
布林值 | 被註釋的元素必須為true |
@AssertFalse |
布林值 | 被註釋的元素必須為false |
@Max(value = , message = "") |
數字 | 被註釋的元素必須是一個數字,且小於或等於最大值 |
@Min(value = , message = "") |
數字 | 被註釋的元素必須是一個數字,且大於或等於最小值 |
@DecimalMax(value = "", message = "") |
數字 | 被註釋的元素必須是一個數字,且小於或等於最大值 |
@DecimalMin(value = "", message = "") |
數字 | 被註釋的元素必須是一個數字,且大於或等於最小值 |
@Pattern(regex=,flag=) |
字串 | 被註釋的元素是否符合正規表示式規則 |
@Size(max=, min=) |
數字 | 被註釋的元素的大小必須在指定範圍內,min 表示最小,max表示最大 |
@Digits (integer, fraction) |
數字 | 被註釋的元素必須是一個數字,且在可接收範圍內 |
@Positive |
數字 | 被註釋的元素必須是正數 |
@PositiveOrZero |
數字 | 被註釋的元素必須是0或正數 |
@Negative |
數字 | 被註釋的元素必須是負數 |
@NegativeOrZero |
數字 | 被註釋的元素必須是0或者負數 |
@Past |
日期 | 被註釋的元素必須是一個過去的日期 |
@PastOrPresent |
日期 | 被註釋的元素必須是一個過去或當前日期 |
@Future |
日期 | 被註釋的元素必須是一個過去的日期 |
@FutureOrPresent |
日期 | 被註釋的元素必須是一個將來或當期的日期 |
看到這些註解後,大家可能會對【@NotNul
、@NotEmpty
、@NotBlank
】這三個註解有點不理解,這裡稍作解釋
@NotNull
:任何物件的value不能為null。@NotEmpty
:集合物件的元素不為0,即集合不為空,也可以用於字串不為null。@NotBlank
:只能用於字串不為null,並且字串trim()以後length要大於0。
五、快速入門
5.1 新增加一個一個User
實體類
@Data
public class User {
//姓名
@NotBlank(message = "使用者名稱不能為空") //註解確保姓名不為空
private String name;
//性別
@NotBlank(message = "性別不能為空") //註解確保性別不為空
private String sex;
//年齡
@NotNull(message = "年齡不能為空") //註解確保年齡不為空
@Max(value = 120,message = "年齡不能大於120") //註解確保年齡必須小於等於120
@Min(value = 18,message = "年齡不能小於18") //註解確保年齡必須大於等於18
private Integer age;
//郵箱
@Email(message = "郵箱格式不正確") //註解確保郵箱格式正確
@NotBlank(message = "郵箱不能為空")
private String email;
}
上述程式碼說明:
@NotBlank
: 此註解確保字串不為空並且不能為空字串,且去掉前後空格後的長度必須大於 0。它常用於字串欄位驗證。message
屬性用於指定提示資訊;@NotNull
: 此註解確保整數型別不能為null
;@Min
和@Max
: 這兩個註解用於驗證數字值是否在指定的範圍內。例如,在上面的示例中,我們想要確保age
的值在 18 到 120 之間;@Email
: 此註解用於驗證字串值是否是有效的電子郵件地址格式。
5.2 Controller
層引數校驗
下圖是controller
層校驗流程
@RestController
public class ValidatorController {
//測試引數校驗
@RequestMapping("/testValidator")
public ResponseEntity<String> testValidator(@Valid @RequestBody User user, BindingResult bindingResult){
// 是否存在校驗錯誤
if (bindingResult.hasErrors()) {
// 獲取校驗不透過欄位的提示資訊
String errorMsg = bindingResult.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(errorMsg);
}
return ResponseEntity.ok("引數校驗成功");
}
}
解釋一下上面程式碼:
@Validated
: 告訴 Spring 需要對User
物件執行校驗; 這個一定不要忘記加上BindingResult
: 該類包含校驗不透過時的異常資訊,校驗不透過時,我們透過這個物件來獲取註解中message="xxx"中的內容
注意:當註解校驗不透過時,直接將異常資訊返回給前端其實並不友好,我們可以將異常包裝一下再丟給前端
5.3 測試校驗結果
這裡我們使用postman工具測試一下引數校驗是否成功
① 入參正確情況
{
"name":"小凡",
"sex":"男",
"age":18,
"email":"xiezhr@qq.com"
}
②入參不正確的情況
{
"name":null,
"sex":"",
"age":17,
"email":"xiezhrqq.com"
}
透過上面的入門小案例,你學會了麼?
上面的返回結果看起來可能不是那麼優雅,那麼怎麼封裝統一返回結果呢,
傳送門在此優雅的封裝返回結果
六、單個引數校驗
上面快速入門中我們說了實體引數校驗,這小節,我們來看看單個引數的校驗
6.1 controller層校驗程式碼
@RequestMapping("/testSingleParmaValidator")
public ResponseEntity<String> testSingleParmaValidator(@NotBlank(message = "姓名不能為空") String name,
@Min(value = 18,message = "年齡不能小於18")
@Max(value = 120,message = "年齡不能大於120") Integer age
){
// 引數校驗
return ResponseEntity.ok("引數校驗成功");
}
6.2 全域性異常捕獲
當引數校驗不透過會發生如下異常資訊
這裡我們不能像上面一樣透過BindingResult
來獲取異常資訊,需要新增全域性異常捕獲校驗失敗異常,具體程式碼如下
@RestControllerAdvice
public class GlobalExceptionHandler {
//處理ValidationException異常
@ExceptionHandler(ValidationException.class)
//返回狀態碼為400
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<String> handleValidationExceptions(ValidationException ex) {
String message = "";
//判斷異常型別
if(ex instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) ex;
//獲取驗證不透過的資訊
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
//遍歷驗證不透過的資訊
for (ConstraintViolation<?> item : violations) {
//將驗證不透過的資訊拼接到message中
message+=item.getMessage()+",";
}
}
//返回錯誤資訊
return ResponseEntity.badRequest().body(message);
}
}
6.3 測試校驗結果
①入參正確情況
http://localhost:8080/testSingleParmaValidator?name=小凡&age=18
②入參不正確情況
http://localhost:8080/testSingleParmaValidator?name=&age=17
八、引數校驗分組
在實際開發中,我們會遇到這樣的情況:同一個實體類可能會在多個介面中使用,但每次的校驗場景又不一樣。
例如:新增使用者和修改使用者介面,引數都是
User
實體,在新增使用者的時候ID
欄位 可以為空,但name
欄位 不能為空在修改使用者的是由
ID
欄位不能為空,這種時候就可以使用引數分組來實現。
8.1 定義驗證分組介面
定義兩個分組介面
CreateUserGroup
(使用者建立組),UpdateUserGroup
(使用者更新組),分別繼承
javax.validation.groups.Default
,標識不同的業務場景
public interface CreateUserGroup extends Default {
}
public interface UpdateUserGroup extends Default {
}
注:繼承Default並不是必須的。只是說,如果繼承了Default
,那麼@Validated(value = Create.class)
的校驗範疇就
為【Create】和【Default】;如果沒繼承Default,那麼@Validated(value = Create.class)
的校驗範疇只
為【Create】,而@Validated(value = {Create.class, Default.class})
的校驗範疇才為【Create】和【Default】
8.2 分組校驗的使用
① 在實體中新增groups
屬性
@Data
public class User {
//使用者ID
@NotNull(message = "使用者ID不能為空",groups = UpdateUserGroup.class) //使用者更新介面必須傳遞使用者ID
private Integer id;
//姓名
@NotBlank(message = "使用者名稱不能為空",groups = CreateUserGroup.class) //使用者建立介面必須傳遞使用者名稱
private String name;
//性別
@NotBlank(message = "性別不能為空") //註解確保性別不為空
private String sex;
//年齡
@NotNull(message = "年齡不能為空") //註解確保年齡不為空
@Max(value = 120,message = "年齡不能大於120") //註解確保年齡必須小於等於120
@Min(value = 18,message = "年齡不能小於18") //註解確保年齡必須大於等於18
private Integer age;
//郵箱
@Email(message = "郵箱格式不正確") //註解確保郵箱格式正確
@NotBlank(message = "郵箱不能為空")
private String email;
}
②在介面中使用分組
使用 @Validated
註解,並指定要執行的驗證組。
//新增使用者
@PostMapping("/addUser")
public ResponseEntity<User> addUser(@Validated(value= CreateUserGroup.class) @RequestBody User user){
return ResponseEntity.ok(user);
}
//更新使用者
@PutMapping("/updateUser")
public ResponseEntity<User> updateUserUser(@Validated(value= UpdateUserGroup.class) @RequestBody User user){
return ResponseEntity.ok(user);
}
我們指定create介面指定CreateUserGroup分組,update介面指定UpdateUserGroup
8.3 測試一下介面
介面入參
{
"name":"小凡",
"sex":"男",
"age":18,
"email":"xiezhr@qq.com"
}
①addUser
介面新增使用者,不需要id,驗證透過
②updateUser
介面修改使用者,需要傳入id,校驗不透過
九、巢狀物件校驗
9.1 構造一個員工資訊表
@Data
public class Emp {
@NotBlank(message = "員工編號不能為空")
private String empNo;
@NotBlank(message = "員工姓名不能為空")
private String empName;
@NotBlank(message = "員工職位不能為空")
private String job;
@Valid //這裡必須使用@Valid註解
private Dept dept;
}
@Data
public class Dept {
@NotBlank(message = "部門編號不能為空")
private String deptNo;
@NotBlank(message = "部門名稱不能為空")
private String deptName;
}
在這個示例中, Dept
類包含三個欄位需要校驗: deptNo
和``deptName欄位,透過在
Dept類中的每個欄位上新增相應的校驗註解,然後在
Emp類中的
dept欄位上新增
@Valid` 註解,可以實現對巢狀物件中多個欄位進行引數校驗。
9.2 巢狀物件的使用
@PostMapping("/emp")
public ResponseEntity<String> createOrder(@Valid @RequestBody Emp emp) {
return ResponseEntity.ok("引數校驗成功");
}
9.3 測試一下
① 正確入參情況
{
"empNo":"10001",
"empName":"小凡",
"job":"程式設計師",
"dept":{
"deptNo":"20001",
"deptName":"研發部111"
}
}
② 不正確入參情況
{
"empNo":"10001",
"empName":"",
"job":"程式設計師",
"dept":{
"deptNo":"20001",
"deptName":""
}
}
十、自定義引數校驗
SpringBoot 提供的註解校驗功能可以滿足大多數的驗證需求,但如果在系統中需要實現一些特殊的校驗功能時,
我們可以根據規則自定義校驗
下面我們來手把手教你自定義一個字串校驗,校驗字串必須為大寫或小寫
10.1 自定義註解類
我們要自定義驗證功能,需要首先自定義註解,以便我們在實體類中使用它,程式碼如下
①定義一個列舉類 CaseMode
:
public enum CaseMode {
UPPER,
LOWER;
}
②建立一個自定義的校驗註解 @CheckCase
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
public @interface CheckCase {
String message() default "字串必須是大寫或小寫";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
}
10.2 自定義驗證業務邏輯類
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
// 獲取約束註解的值
this.caseMode = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果值為空,則返回true
if (value == null) {
return true;
}
// 根據caseMode的值,判斷value是否需要轉換大小寫
if (caseMode == CaseMode.UPPER) {
return value.equals(value.toUpperCase());
} else {
return value.equals(value.toLowerCase());
}
}
}
10.3 自定義校驗註解使用
①在Car
實體類上新增註解
@Data
public class Car {
//車牌號
@CheckCase(value = CaseMode.UPPER,message = "車牌號必須為大寫")
private String brand;
//顏色
@CheckCase(value = CaseMode.LOWER,message = "顏色必須為小寫")
private String color;
}
②在controller
中校驗引數
@GetMapping ("/car")
public ResponseEntity<String> validatorCar(@Valid @RequestBody Car car) {
return ResponseEntity.ok("引數校驗成功");
}
10.4 測試一下
①入參正確情況
{
"brand":"雲A.888888",
"color":"red"
}
②入參錯誤情況
{
"brand":"雲a.888888",
"color":"RED"
}
以上就是本期的全部內容,希望對你有所幫助,我們下期再見 (●'◡'●)