天天看點

Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

目錄 #%E2%80%8B 1. 結論先出 Valid VS Validated 相同點 JSR 380 Valid VS Validated 不同點? @Valid和@Validated差別 Validator 2. @Valid和@Validated 注解 3. 例子 4.使用@Valid嵌套校驗 5. 組合使用@Valid和@Validated 進行集合校驗 6. 自定義校驗 空與非空檢查 Boolean值檢查 日期檢查 數值檢查 其他 hibernate-validator擴充限制(部分) 自定義限制注解 工作原理  結論

Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

  • 都可以對方法和參數進行校驗
  • @Valid和@Validated 兩種注釋都會導緻應用标準Bean驗證。

    如果驗證不通過會抛出

    BindException

    異常,并變成400(BAD_REQUEST)響應;或者可以通過

    Errors

    BindingResult

    參數在控制器内本地處理驗證錯誤。另外,如果參數前有

    @RequestBody

    注解,驗證錯誤會抛出

    MethodArgumentNotValidException

    異常。

JSR 380 是用于 bean 驗證的 Java API 規範,是 Jakarta EE 和 JavaSE 的一部分。這確定 bean 的屬性滿足特定條件,使用諸如@NotNull、@Min和@Max 之類的注釋。

此版本需要 Java 8 或更高版本,并利用 Java 8 中添加的新功能,例如類型注釋和對Optional和LocalDate等新類型的支援。

有關規範的完整資訊,請繼續閱讀

  • javax.validation.Valid

    • 是JSR-303規範标準注解支援,是一個标記注解。
    • 注解支援

      ElementType#METHOD

      ,

      ElementType#FIELD

      ElementType#CONSTRUCTOR

    • ElementType#PARAMETER

      ElementType#TYPE_USE

  • org.springframework.validation.annotation.Validated

    • 是Spring 做得一個自定義注解,增強了分組功能。
    • ElementType#TYPE

      ElementType#METHOD

      ElementType#PARAMETER

@Valid

@Validated

差別

@Valid @Validated
提供者 JSR-303規範 Spring
是否支援分組 不支援 支援
标注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
嵌套校驗

Bean Validation 2.0(JSR 380)定義了用于實體和方法驗證的中繼資料模型和API,Hibernate Validator是目前最好的實作

Validator

接口有三個方法,可用于驗證整個實體或僅驗證明體的單個屬性

  • Validator#validate()

    驗證所有bean的所有限制
  • Validator#validateProperty()

    驗證單個屬性
  • Validator#validateValue()

    檢查給定類的單個屬性是否可以成功驗證
不管是

requestBody參數校驗

還是

方法級别的校驗

,最終都是調用

Hibernate Validator

執行校驗,

Spring Validation

隻是做了一層封裝。

驗證使用者的輸入是我們大多數應用程式中的常見功能。在 Java 生态系統中,我們專門使用

Java Standard Bean Validation API

來支援這一點。此外,從 4.0 版本開始,這也與 Spring 很好地內建在一起.

在接下來的部分中,讓我們詳細了解它們。

2. @Valid和@Validated 注解

在 Spring 中,我們使用 JSR-303 的@Valid注釋進行方法級别驗證。此外,我們還使用它來标記成員屬性以進行驗證。但是,此注釋不支援組驗證。

組有助于限制驗證期間應用的限制。一個特殊的用例是 UI 界面(UI wizards)。在這裡,在第一步中,我們可能有某個字段子組。在後續步驟中,可能有另一個組屬于同一個 bean。是以我們需要在每一步中對這些有限的字段應用限制,但@Valid不支援這一點。

在這種情況下,對于組級别,我們必須使用 Spring 的@Validated,它是 JSR-303 的@Valid的變體。這是在方法級别使用的。對于标記成員屬性,我們繼續使用@Valid注釋。

現在,讓我們直接進入并通過一個例子來看看這些注解的用法。

讓我們考慮一個使用 Spring Boot 開發的簡單使用者注冊。首先,我們将隻有名稱和密碼屬性:

public class UserAccount {

    @NotNull

    @Size(min = 4, max = 15)

    private String password;

    @NotBlank

    private String name;

    // standard constructors / setters / getters / toString

}

接下來,讓我們看看控制器。在這裡,我們将使用帶有@Valid注釋的saveBasicInfo方法來驗證使用者輸入:

@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)

public String saveBasicInfo(

  @Valid @ModelAttribute("useraccount") UserAccount useraccount, 

  BindingResult result, 

  ModelMap model) {

    if (result.hasErrors()) {

        return "error";

    }

    return "success";

現在讓我們測試這個方法:

@Test

public void givenSaveBasicInfo_whenCorrectInput_thenSuccess() throws Exception {

    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")

      .accept(MediaType.TEXT_HTML)

      .param("name", "test123")

      .param("password", "pass"))

      .andExpect(view().name("success"))

      .andExpect(status().isOk())

      .andDo(print());

确認測試運作成功後,我們現在擴充功能。下一個合乎邏輯的步驟是将其轉換為複雜使用者注冊。第一步,名稱和密碼保持不變。在第二步中,我們将擷取諸如年齡 和 電話之類的附加資訊。是以,我們将使用這些附加字段更新我們的域對象:

public class UserAccount {

    @Min(value = 18, message = "Age should not be less than 18")

    private int age;

    private String phone;

    // standard constructors / setters / getters / toString  

但是,這一次我們會注意到之前的測試失敗了。這是因為我們沒有傳入age和phone字段。為了支援這種行為,我們需要組驗證和@Validated注釋。

為此,我們需要對字段進行分組,建立兩個不同的組。首先,我們需要建立兩個标記接口。每個組或每個步驟單獨一個。我們可以參考我們關于

組驗證

的文章以了解具體的實作方式。在這裡,讓我們關注注釋的差異。

我們将有第一步的BasicInfo接口和第二步的 AdvanceInfo  。此外,我們将更新UserAccount類以使用這些标記接口,如下所示:

public class UserAccount {

    @NotNull(groups = BasicInfo.class)

    @Size(min = 4, max = 15, groups = BasicInfo.class)

    @NotBlank(groups = BasicInfo.class)

    @Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)

    @NotBlank(groups = AdvanceInfo.class)

此外,我們現在将更新我們的控制器以使用@Validated批注而不是@Valid:

@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)

public String saveBasicInfoStep1(

  @Validated(BasicInfo.class)

  @ModelAttribute("useraccount") UserAccount useraccount, 

  BindingResult result, ModelMap model) {

由于此更新,我們的測試現在成功運作。現在讓我們也測試一下這個新方法:

@Test

public void givenSaveBasicInfoStep1_whenCorrectInput_thenSuccess() throws Exception {

    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")

這也運作成功。是以,我們可以看到@Validated的使用 對于組驗證至關重要。

接下來,讓我們看看@Valid如何觸發嵌套屬性的驗證。

@Valid注釋用于校驗嵌套屬性。這會觸發嵌套對象的驗證。例如,在我們目前的場景中,讓我們建立一個 UserAddress 對象:

public class UserAddress {

    private String countryCode;

為了確定此嵌套對象的驗證,我們将使用@Valid注釋來裝飾該屬性:

public class UserAccount {

    //...

    @Valid

    @NotNull(groups = AdvanceInfo.class)

    private UserAddress useraddress;

    // standard constructors / setters / getters / toString

5. 組合使用@Valid和@Validated 進行集合校驗

如果請求體直接傳遞了

json

數組給背景,并希望對數組中的每一項都進行參數校驗。此時,如果我們直接使用

java.util.Collection

下的

list

或者

set

來接收資料,參數校驗并不會生效!我們可以使用自定義

list

集合來接收參數:

  • 包裝

    List

    類型,并聲明

    @Valid

    注解
package com.devicemag.core.BO;
 
import javax.validation.Valid;
import java.util.*;
 
/**
 * @Title: 參數校驗工具類, 用于校驗List<E> 類型的請求參數
 * @ClassName: com.devicemag.core.BO.ValidList.java
 * @Description:
 *
 * @Copyright 2020-2021 - Powered By 研發中心
 * @author: 王延飛
 * @date: 2020/12/25 20:23
 * @version V1.0
 */
public class ValidList<E> implements List<E> {
 
    @Valid
    private List<E> list = new ArrayList<>();
 
    @Override
    public int size() {
        return list.size();
    }
 
    @Override
    public boolean isEmpty() {
        return list.isEmpty();
    }
 
    @Override
    public boolean contains(Object o) {
        return list.contains(o);
    }
 
    @Override
    public Iterator<E> iterator() {
        return list.iterator();
    }
 
    @Override
    public Object[] toArray() {
        return list.toArray();
    }
 
    @Override
    public <T> T[] toArray(T[] a) {
        return list.toArray(a);
    }
 
    @Override
    public boolean add(E e) {
        return list.add(e);
    }
 
    @Override
    public boolean remove(Object o) {
        return list.remove(o);
    }
 
    @Override
    public boolean containsAll(Collection<?> c) {
        return list.containsAll(c);
    }
 
    @Override
    public boolean addAll(Collection<? extends E> c) {
        return list.addAll(c);
    }
 
    @Override
    public boolean addAll(int index, Collection<? extends E> c) {
        return list.addAll(index, c);
    }
 
    @Override
    public boolean removeAll(Collection<?> c) {
        return list.removeAll(c);
    }
 
    @Override
    public boolean retainAll(Collection<?> c) {
        return list.retainAll(c);
    }
 
    @Override
    public void clear() {
        list.clear();
    }
 
    @Override
    public E get(int index) {
        return list.get(index);
    }
 
    @Override
    public E set(int index, E element) {
        return list.set(index, element);
    }
 
    @Override
    public void add(int index, E element) {
        list.add(index, element);
    }
 
    @Override
    public E remove(int index) {
        return list.remove(index);
    }
 
    @Override
    public int indexOf(Object o) {
        return list.indexOf(o);
    }
 
    @Override
    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }
 
    @Override
    public ListIterator<E> listIterator() {
        return list.listIterator();
    }
 
    @Override
    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index);
    }
 
    @Override
    public List<E> subList(int fromIndex, int toIndex) {
        return list.subList(fromIndex, toIndex);
    }
 
    public List<E> getList() {
        return list;
    }
 
    public void setList(List<E> list) {
        this.list = list;
    }
     // 一定要記得重寫toString方法
 
    @Override
    public String toString() {
        return "ValidList{" +
                "list=" + list +
                '}';
    }
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

比如,我們需要一次性儲存多個

UserAccount

對象,

Controller

層的方法可以這麼寫:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserAccount.class) ValidationList<UserAccount > userList) {
// 校驗通過,才會執行業務邏輯處理
return Result.ok();
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

validator-api-2.0的限制注解有22個,具體我們看下面表格

支援Java類型 說明
@Null Object 為null
@NotNull 不為null
@NotBlank CharSequence 不為null,且必須有一個非空格字元
@NotEmpty CharSequence、Collection、Map、Array 不為null,且不為空(length/size>0)

備注
@AssertTrue boolean、Boolean 為true 為null有效
@AssertFalse 為false

@Future Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 驗證日期為目前時間之後
@FutureOrPresent 驗證日期為目前時間或之後
@Past 驗證日期為目前時間之前
@PastOrPresent 驗證日期為目前時間或之前

@Max BigDecimal、BigInteger,byte、short、int、long以及包裝類 小于或等于
@Min 大于或等于
@DecimalMax BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包裝類
@DecimalMin
@Negative BigDecimal、BigInteger,byte、short、int、long、float、double以及包裝類 負數 為null有效,0無效
@NegativeOrZero 負數或零
@Positive 正數
@PositiveOrZero 正數或零
@Digits(integer = 3, fraction = 2) 整數位數和小數位數上限

@Pattern 比對指定的正規表達式
@Email 郵箱位址 為null有效,預設正則

'.*'

@Size 大小範圍(length/size>0)

@Length String 字元串長度範圍
@Range 數值類型和String 指定範圍
@URL URL位址驗證

除了以上提供的限制注解(大部分情況都是能夠滿足的),我們還可以根據自己的需求自定義自己的限制注解

定義自定義限制,有三個步驟

  • 建立限制注解
  • 實作一個驗證器
  • 定義預設的錯誤資訊

那麼下面就直接來定義一個簡單的驗證手機号碼的注解

@Documented
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Constraint(validatedBy = {MobileValidator.class})
@Retention(RUNTIME)
@Repeatable(Mobile.List.class)
public @interface Mobile {
    /**
     * 錯誤提示資訊,可以寫死,也可以填寫國際化的key
     */
    String message() default "手機号碼不正确";
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
    String regexp() default "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Mobile[] value();
    }
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

關于注解的配置這裡不說了,自定義限制需要下面3個屬性

  • message

    錯誤提示資訊,可以寫死,也可以填寫國際化的key
  • groups

    分組資訊,允許指定此限制所屬的驗證組(下面會說到分組限制)
  • payload

    有效負載,可以通過payload來标記一些需要特殊處理的操作
  • @Repeatable

    注解和

    List

    定義可以讓該注解在同一個位置重複多次,通常是不同的配置(比如不同的分組和消息)
  • @Constraint(validatedBy = {MobileValidator.class})

    該注解是指明我們的自定義限制的驗證器,那下面就看一下驗證器的寫法,需要實作

    javax.validation.ConstraintValidator

    接口
public class MobileValidator implements ConstraintValidator<Mobile, String> {
    /**
     * 手機驗證規則
     */
    private Pattern pattern;
    @Override
    public void initialize(Mobile mobile) {
        pattern = Pattern.compile(mobile.regexp());
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        return pattern.matcher(value).matches();
    }
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

ConstraintValidator

接口定義了在實作中設定的兩個類型參數。

  1. 第一個指定要驗證的注解類(如

    Mobile

    ),
  2. 第二個指定驗證器可以處理的元素類型(如

    String

    );

    initialize()

    方法可以通路限制注解的屬性值;

    isValid()

    方法用于驗證,傳回true表示驗證通過
Bean驗證規範建議将空值視為有效。如果

null

不是元素的有效值,則應使用

@NotNull

顯式注釋

到這裡我們自定義的限制就寫好了,可以用個例子來測試一下

public class MobileTest {
    public void setMobile(@Mobile String mobile){
        // to do
    }
    private static ExecutableValidator executableValidator;
    @BeforeAll
    public static void setUpValidator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        executableValidator = factory.getValidator().forExecutables();
    }
    @Test
    public void manufacturerIsNull() throws NoSuchMethodException {
        MobileTest mobileTest = new MobileTest();
        Method method = MobileTest.class.getMethod("setMobile", String.class);
        Object[] parameterValues = {"1111111"};
        Set<ConstraintViolation<MobileTest>> violations = executableValidator.validateParameters(
                mobileTest, method, parameterValues);
        violations.forEach(violation -> System.out.println(violation.getMessage()));
    }
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論
手機号碼不正确      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

@Validated

的工作原理

方法級别參數校驗

在每個參數前面聲明限制注解,然後通過解析參數注解完成校驗,這就是方法級别的參數校驗。 這種方式可以用于任何的

Spring Bean

的方法上,一般來說,這種方式一般會采用

AOP

Around

增強完成 在Spring中,是通過以下步驟完成

  1. MethodValidationPostProcessor

    在Bean的初始化完成之後,判斷是否要進行AOP代理(類是否被

    @Validated

    标記)
  2. MethodValidationInterceptor

    攔截所有方法,執行校驗邏輯
  3. 委派

    Validator

    執行參數校驗和傳回值校驗,得到

    ConstraintViolation

  4. 處理

    ConstraintViolation

總之,對于任何基本驗證,我們将在方法調用中使用 JSR @Valid注釋。另一方面,對于任何組驗證,包括

組序列

,我們需要 在我們的方法調用中使用 Spring 的@Validated注釋。所述@Valid 還需要注釋來觸發嵌套屬性的驗證。

  • @Validated

    的原理本質還是

    AOP

    。在方法校驗上,利用AOP動态攔截方法,利用

    JSR303 Validator

    實作完成校驗。在Bean的屬性校驗上,則是基于Bean的生命周期,在其初始化前後完成校驗
  • Spring Validator

    本質實作還是

    JSR303 Validaotr

    ,隻是能讓其更好的适配

    Spring Context

  • @javax.validation.Valid

    JSR303

    的核心标記注解,但是在

    Spring Framework

    中被

    @Validated

    取代,但是

    Spring Validator

    的實作可以支援相容

    @javax.validation.Valid

例如,在

MethodValidationPostProcessor

提供了

setValidatedAnnotationType

方法,替換預設的

@Validated

Spring MVC

中,

RequestResponseBodyMethodProcessor

@RequestBody

@ResponseBody

的校驗處理,就相容了

@javax.validation.Valid

@Validated

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
            if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
                Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
                Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
                binder.validate(validationHints);
                break;
            }
        }
    }
}      
Validated、Valid 、Validator,他們的差別你知道幾個1. 結論先出2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校驗5. 組合使用@Valid和@Validated 進行集合校驗6. 自定義校驗工作原理 結論

參考連結: 

https://www.baeldung.com/spring-valid-vs-validated https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/annotation/Validated.html https://docs.oracle.com/javaee/7/api/javax/validation/Valid.html https://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/javax/validation/Validator.html https://reflectoring.io/bean-validation-with-spring-boot/ https://jcp.org/en/jsr/detail?id=380 https://www.baeldung.com/javax-validation