SpringBoot參數校驗-Validator


前言

在日常的開發中,服務端對象的校驗是非常重要的一個環節,比如用戶注冊的時候:校驗用戶名,密碼,身份證,郵箱等信息是否為空,以及格式是否正確,但是這種在日常的開發中進行校驗太繁瑣了,代碼繁瑣而且很多。Validator框架應運而生,它的出現就是為了解決開發人員在開發的時候減少代碼的,提升開發效率。它專門用來做接口的參數校驗,比如:密碼長度、是否為空等等。

概述

JSR303 定義了 Bean Validation(校驗)的標准 validation-api,但並沒有提供實現。Hibernate Validation是對這個規范的實現 ,並且增加了 @Email@Length@Range 等注解。Spring Validation 底層依賴的就是Hibernate Validation

  • JSR303:JSR303是一項標准,只提供規范不提供實現。定義了校驗規范即校驗注解如:@Null、@NotNull、@Pattern。位於:javax.validation.constraints包下。
  • hibernate validation:是對 JSR303 規范的實現並且進行了增強和擴展。並增加了注解:@Email、@Length、@Range等。
  • spring Validation:是對Hibernate Validation的二次封裝。在SpringMvc模塊中添加了自動校驗。並將校驗信息封裝到特定的類中。

常用約束說明

JSR 提供的校驗注解:

  • @Null 被注釋的元素必須為 null
  • @NotNull 被注釋的元素必須不為 null
  • @AssertTrue 被注釋的元素必須為 true
  • @AssertFalse 被注釋的元素必須為 false
  • @Min(value) 被注釋的元素必須是一個數字,其值必須大於等於指定的最小值
  • @Max(value) 被注釋的元素必須是一個數字,其值必須小於等於指定的最大值
  • @DecimalMin(value) 被注釋的元素必須是一個數字,其值必須大於等於指定的最小值
  • @DecimalMax(value) 被注釋的元素必須是一個數字,其值必須小於等於指定的最大值
  • @Size(max=, min=) 被注釋的元素的大小必須在指定的范圍內
  • @Digits (integer, fraction) 被注釋的元素必須是一個數字,其值必須在可接受的范圍內
  • @Past 被注釋的元素必須是一個過去的日期
  • @Future 被注釋的元素必須是一個將來的日期
  • @Pattern(regex=,flag=) 被注釋的元素必須符合指定的正則表達式

Hibernate Validator提供的校驗注解:

  • @NotBlank(message =) 驗證字符串非 null,且長度必須大於 0
  • @Email 被注釋的元素必須是電子郵箱地址
  • @Length(min=,max=) 被注釋的字符串的大小必須在指定的范圍內
  • @NotEmpty 被注釋的字符串的必須非空
  • @Range(min=,max=,message=) 被注釋的元素必須在合適的范圍內

實戰開發

@Validated注解用法

引入依賴
  • SpringBoot提供了validator啟動器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • 非SpringBoot項目,需要自行引入依賴
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.1.5.Final</version>
</dependency>
<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>jakarta.el</artifactId>
    <version>3.0.3</version>
</dependency> 
實體約束示例
package com.lzy.entity;

import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;

/**
 * created by Luzy on 2021-07-10 19:41
 */
@Data
public class Student {
    @NotBlank(message = "用戶名不能為空")
    private String name;
    @Min(value = 18, message = "年齡不能小於18歲")
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手機號格式錯誤")
    private String phone;
    @Email(message = "郵箱格式錯誤")
    private String email;
    @Valid
    @NotNull
    private School school;

    @Data
    private static class School{
        @NotBlank(message = "學校名不能為空")
        private String name;
        @NotBlank(message = "學校地址不能為空")
        private String address;
    }
}
校驗演示

准備Contoller進行測試

package com.lzy.controller;

import com.lzy.config.Update;
import com.lzy.entity.Student;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;

/**
 * created by Luzy on 2021-07-10 19:41
 */
@RestController
@Slf4j
public class TestController { 
}
  • 當Controller層方法中參數是對象時(如Student,且對象中字段已經加了約束),若不加@Validated注解,則約束不起效果
@RestController
@Slf4j
public class TestController {
    @GetMapping("/t1")
    public String test3(Student student) {
        log.info("學生信息:{}", student);
        return "ok";
    }    
}

image-20210718152924485

2021-07-18 15:20:38.740  INFO 3212 --- [nio-8080-exec-8] com.lzy.controller.TestController        : 學生信息:Student(id=, name=, age=null, phone=, email=, school=Student.School(name=, address=))
  • 對象參數前加上@Validated注解后約束生效
@RestController
@Slf4j
public class TestController {
    @GetMapping("/t2")
    public String test1(@Validated Student student) {
        log.info("學生信息:{}", student);
        return "ok";
    }

    @GetMapping("/t3")
    public String test2(@Validated @RequestBody Student student) {
        log.info("學生信息:{}", student);
        return "ok";
    }
  
}

image-20210718153124148

2021-07-18 15:30:53.440  WARN 3212 --- [io-8080-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 6 errors
Field error in object 'student' on field 'name': rejected value []; codes [NotBlank.student.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.name,name]; arguments []; default message [name]]; default message [用戶名不能為空]
Field error in object 'student' on field 'school.name': rejected value []; codes [NotBlank.student.school.name,NotBlank.school.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.name,school.name]; arguments []; default message [school.name]]; default message [學校名不能為空]
Field error in object 'student' on field 'school.address': rejected value []; codes [NotBlank.student.school.address,NotBlank.school.address,NotBlank.address,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.address,school.address]; arguments []; default message [school.address]]; default message [學校地址不能為空]
Field error in object 'student' on field 'age': rejected value [11]; codes [Min.student.age,Min.age,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.age,age]; arguments []; default message [age],18]; default message [年齡不能小於18歲]
Field error in object 'student' on field 'email': rejected value [1234qq.com]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@2e90ef80,.*]; default message [郵箱格式錯誤]
Field error in object 'student' on field 'phone': rejected value []; codes [Pattern.student.phone,Pattern.phone,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.phone,phone]; arguments []; default message [phone],[Ljavax.validation.constraints.Pattern$Flag;@5e6ea6fb,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [手機號格式錯誤]]
  • Json格式傳遞參數同樣
@RestController
@Slf4j
public class TestController {
    @GetMapping("/t3")
    public String test2(@Validated @RequestBody Student student) {
        log.info("學生信息:{}", student);
        return "ok";
    } 
}

image-20210718161127201

2021-07-18 15:32:56.979  WARN 3212 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.lzy.controller.TestController.test2(com.lzy.entity.Student) with 3 errors: [Field error in object 'student' on field 'name': rejected value []; codes [NotBlank.student.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.name,name]; arguments []; default message [name]]; default message [用戶名不能為空]] [Field error in object 'student' on field 'phone': rejected value [18812345]; codes [Pattern.student.phone,Pattern.phone,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.phone,phone]; arguments []; default message [phone],[Ljavax.validation.constraints.Pattern$Flag;@5e6ea6fb,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [手機號格式錯誤]] [Field error in object 'student' on field 'school.name': rejected value []; codes [NotBlank.student.school.name,NotBlank.school.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.school.name,school.name]; arguments []; default message [school.name]]; default message [學校名不能為空]] ]

  • 如果想對Controller層方法中參數直接進行約束,此時必須在類上添加@Validated,否則約束不起效果
@RestController
@Slf4j
public class TestController {

    @GetMapping("/t4")
    public String test4(@NotBlank(message = "用戶名不能為空") String name,
                        @Min(value = 18, message = "年齡不能小於18歲") Integer age) {
        log.info("學生{}的年齡為{}", name,age);
        return "ok";
    }

}

image-20210718155833969

2021-07-18 15:49:53.751  INFO 21548 --- [nio-8080-exec-7] com.lzy.controller.TestController        : 學生null的年齡為16
  • 在類上加上@Validated后約束生效
@RestController
@Slf4j
@Validated
public class TestController {
    @GetMapping("/t4")
    public String test4(@NotBlank(message = "用戶名不能為空") String name,
                        @Min(value = 18, message = "年齡不能小於18歲") Integer age) {
        log.info("學生{}的年齡為{}", name,age);
        return "ok";
    }
}
2021-07-18 15:55:20.738 ERROR 16840 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: test4.name: 用戶名不能為空, test4.age: 年齡不能小於18歲] with root cause

javax.validation.ConstraintViolationException: test4.name: 用戶名不能為空, test4.age: 年齡不能小於18歲
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.8.jar:5.3.8]....

@Validated與@Valid

@Valid注解與@Validated注解功能大部分類似;

不同點:

  • @Valid屬於javax包下,而@Validated屬於Spring下
  • @Valid支持嵌套校驗、而@Validated不支持
  • @Validated支持分組,而@Valid不支持

自定義約束注解

  • 創建自定義注解@Phone
package com.lzy.annotation;

import com.lzy.handler.PhoneValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * created by Luzy on 2021-07-18 16:31
 *
 * @Description: 自定義注解:校驗手機號碼格式
 */
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "手機格式不正確!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}
  • 定義具體的驗證器
package com.lzy.handler;

import com.lzy.annotation.Phone;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * created by Luzy on 2021-07-18 16:32
 *
 * @Description: 自定義注解校驗器
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    @Override
    public boolean isValid(String phoneNum, ConstraintValidatorContext constraintValidatorContext) {

        // 1: 如果用戶沒輸入直接返回不校驗,因為空的判斷交給@NotNull去做就行了
        if (phoneNum == null && phoneNum.length() == 0) {
            return true;
        }
        Pattern p = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$");
        // 2:如果校驗通過就返回true,否則返回false;
        Matcher matcher = p.matcher(phoneNum);
        return matcher.matches();
    }

    @Override
    public void initialize(Phone constraintAnnotation) {
    }
}
  • 使用及測試
@RestController
@Slf4j
@Validated
public class TestController {
    @GetMapping("/phone")
    public String phone(@Phone(message = "手機號別瞎jb填") String phoneNum) {
        log.info("學生手機號碼:{}", phoneNum);
        return "ok";
    }
}

image-20210718174842835

2021-07-18 17:48:47.890 ERROR 18844 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: phone.phoneNum: 手機號別瞎jb填] with root cause

javax.validation.ConstraintViolationException: phone.phoneNum: 手機號別瞎jb填
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8]....

多級嵌套模型校驗

前面已經出現過,Student對象中的school屬性也是一個對象,如果要想School對象里的name,adress上的約束生效,則必須要在school上添加 @Valid注解,

且上文已說過,@Valid支持嵌套校驗、而@Validated不支持, 當然此處應該加上一個 @NotNull,避免school對象為null

package com.lzy.entity;

import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;

/**
 * created by Luzy on 2021-07-10 19:41
 */
@Data
public class Student {
    @NotBlank(message = "用戶名不能為空")
    private String name;
    @Min(value = 18, message = "年齡不能小於18歲")
    private Integer age;
    @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手機號格式錯誤")
    private String phone;
    @Email(message = "郵箱格式錯誤")
    private String email;
    @Valid
    @NotNull
    private School school;

    @Data
    private static class School{
        @NotBlank(message = "學校名不能為空")
        private String name;
        @NotBlank(message = "學校地址不能為空")
        private String address;
    }
}

分組校驗

場景說明:比如我們在業務開發中對用戶的新增和修改操作,新增用戶時肯定沒有用戶id,修改用戶時肯定要傳入用戶id,此時便可以使用分組校驗

  • 自定義兩個分組Create和Update
package com.lzy.config;

import javax.validation.groups.Default;
/**
 * created by Luzy on 2021-07-10 19:41
 * @Description: 自定義分組 Create
 */
public interface Create extends Default {
}
package com.lzy.config;

import javax.validation.groups.Default;
/**
 * created by Luzy on 2021-07-10 19:41
 * @Description: 自定義分組 Default
 */
public interface Update extends Default {
}
  • 在之前的Student對象模型中id屬性上指定group為Update
@Data
public class Student {

    @NotBlank(message = "id不能為空",groups ={Update.class} )
    private String id;

    @NotBlank(message = "用戶名不能為空")
    private String name;

    @Min(value = 18, message = "年齡不能小於18歲")
    private Integer age;

    @Phone
    private String phone;

    @Email(message = "郵箱格式錯誤")
    private String email;

    @Valid
    @NotNull
    private School school;

    @Data
    private static class School{
        @NotBlank(message = "學校名不能為空")
        private String name;
        @NotBlank(message = "學校地址不能為空")
        private String address;
    }
}
  • 在Controller啟動校驗時,指定校驗分組
@PostMapping("/createStudent")
public String createStudent(@Validated @RequestBody Student student) {
    log.info("學生信息:{}", student);
    return "createStudent success!";
}


@PostMapping("/updateStudent")
public String updateStudent(@Validated(value = Update.class) @RequestBody Student student) {
    log.info("學生信息:{}", student);
    return "updateStudent success!";
}

image-20210718181510917

2021-07-18 18:15:00.239  INFO 23140 --- [nio-8080-exec-3] com.lzy.controller.TestController        : 學生信息:Student(id=, name=xxxxxx@qq.com, age=18, phone=18158872278, email=xxxxxx@qq.com, school=Student.School(name=中科大, address=安徽省合肥市))

image-20210718181555653

2021-07-18 18:15:48.322  WARN 23140 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.lzy.controller.TestController.updateStudent(com.lzy.entity.Student): [Field error in object 'student' on field 'id': rejected value []; codes [NotBlank.student.id,NotBlank.id,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.id,id]; arguments []; default message [id]]; default message [id不能為空]] ]
  • 關於自定義分組繼承Default說明

繼承Default並不是必須的。

如果繼承了Default,那么@Validated(value = Create.class)的校驗范疇為【Create】和【Default】;
如果沒繼承Default,那么@Validated(value = Create.class)的校驗范疇為【Create】;

Student對象中的name、age、phone等屬性默認分組為Default,如果自定義分組沒有繼承Default,那在Contoller的方法中指定校驗分組時,必須加上Default分組,否則name、age、phone等屬性約束會失效

處理校驗拋出的異常

當注解校驗不通過時,直接將異常信息返回給前端並不友好,我們可以將異常包裝處理后返回給前端。

  • 方式一:使用BindingResult類
    @PostMapping("/t6")
    public String test6(@Validated @RequestBody Student student,BindingResult bindingResult) {
        if (bindingResult.hasErrors()){
            List<ObjectError> allErrors = bindingResult.getAllErrors();
            StringBuilder builder = new StringBuilder();
            allErrors.forEach(e->{
                log.error(e.getDefaultMessage());
                builder.append("【"+e.getDefaultMessage()+"】");
            });
            return builder.toString();
        }
        log.info("學生信息:{}", student);
        return "ok";
    }

image-20210718190759613

image-20210718190856356

注意:

@Validated(或@Valid) 和 BindingResult 是成對出現的,如果有多個@Validated,那么每個@Validated后面都需要添加BindingResult用於接收對象中的校驗信息

  • 方式二(推薦):全局異常處理

詳情可見系列文章《SpringBoot全局異常處理》

Renference


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM