1. 結論先出
Valid VS Validated 相同點
都可以對方法和參數進行校驗
@Valid和@Validated
兩種注釋都會導致應用標準Bean驗證。
如果驗證不通過會拋出BindException異常,并變成400(BAD_REQUEST)響應;或者可以通過Errors或BindingResult參數在控制器內本地處理驗證錯誤。另外,如果參數前有@RequestBody注解,驗證錯誤會拋出MethodArgumentNotValidException異常。
JSR 380
JSR 380 是用于 bean 驗證的 Java API 規范,是 Jakarta EE 和 JavaSE 的一部分。這確保 bean 的屬性滿足特定條件,使用諸如@NotNull、@Min和@Max 之類的注釋。
此版本需要 Java 8 或更高版本,并利用 Java 8 中添加的新功能,例如類型注釋和對Optional和LocalDate等新類型的支持。
有關規范的完整信息,請繼續閱讀JSR 380。
Valid VS Validated 不同點?
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 |
嵌套校驗 | 支持 | 不支持 |
Validator
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注釋。
現在,讓我們直接進入并通過一個例子來看看這些注解的用法。
3. 例子
讓我們考慮一個使用 Spring Boot 開發的簡單用戶注冊。首先,我們將只有名稱和密碼屬性:
1
2
3
4
5
6
7
8
9
10
11
|
public class UserAccount { @NotNull @Size (min = 4 , max = 15 ) private String password; @NotBlank private String name; // standard constructors / setters / getters / toString } |
接下來,讓我們看看控制器。在這里,我們將使用帶有@Valid注釋的saveBasicInfo方法來驗證用戶輸入:
1
2
3
4
5
6
7
8
9
10
|
@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" ; } |
現在讓我們測試這個方法:
1
2
3
4
5
6
7
8
9
10
|
@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()); } |
確認測試運行成功后,我們現在擴展功能。下一個合乎邏輯的步驟是將其轉換為復雜用戶注冊。第一步,名稱和密碼保持不變。在第二步中,我們將獲取諸如年齡 和 電話之類的附加信息。因此,我們將使用這些附加字段更新我們的域對象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class UserAccount { @NotNull @Size (min = 4 , max = 15 ) private String password; @NotBlank private String name; @Min (value = 18 , message = "Age should not be less than 18" ) private int age; @NotBlank private String phone; // standard constructors / setters / getters / toString } |
但是,這一次我們會注意到之前的測試失敗了。這是因為我們沒有傳入age和phone字段。為了支持這種行為,我們需要組驗證和@Validated注釋。
為此,我們需要對字段進行分組,創建兩個不同的組。首先,我們需要創建兩個標記接口。每個組或每個步驟單獨一個。我們可以參考我們關于組驗證的文章以了解具體的實現方式。在這里,讓我們關注注釋的差異。
我們將有第一步的BasicInfo接口和第二步的 AdvanceInfo 。此外,我們將更新UserAccount類以使用這些標記接口,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class UserAccount { @NotNull (groups = BasicInfo. class ) @Size (min = 4 , max = 15 , groups = BasicInfo. class ) private String password; @NotBlank (groups = BasicInfo. class ) private String name; @Min (value = 18 , message = "Age should not be less than 18" , groups = AdvanceInfo. class ) private int age; @NotBlank (groups = AdvanceInfo. class ) private String phone; // standard constructors / setters / getters / toString } |
此外,我們現在將更新我們的控制器以使用@Validated批注而不是@Valid:
1
2
3
4
5
6
7
8
9
10
|
@RequestMapping (value = "/saveBasicInfoStep1" , method = RequestMethod.POST) public String saveBasicInfoStep1( @Validated (BasicInfo. class ) @ModelAttribute ( "useraccount" ) UserAccount useraccount, BindingResult result, ModelMap model) { if (result.hasErrors()) { return "error" ; } return "success" ; } |
由于此更新,我們的測試現在成功運行。現在讓我們也測試一下這個新方法:
1
2
3
4
5
6
7
8
9
10
|
@Test public void givenSaveBasicInfoStep1_whenCorrectInput_thenSuccess() throws Exception { this .mockMvc.perform(MockMvcRequestBuilders.post( "/saveBasicInfoStep1" ) .accept(MediaType.TEXT_HTML) .param( "name" , "test123" ) .param( "password" , "pass" )) .andExpect(view().name( "success" )) .andExpect(status().isOk()) .andDo(print()); } |
這也運行成功。因此,我們可以看到@Validated的使用 對于組驗證至關重要。
接下來,讓我們看看@Valid如何觸發嵌套屬性的驗證。
4.使用@Valid嵌套校驗
@Valid注釋用于校驗嵌套屬性。這會觸發嵌套對象的驗證。例如,在我們當前的場景中,讓我們創建一個 UserAddress 對象:
1
2
3
4
5
6
7
|
public class UserAddress { @NotBlank private String countryCode; // standard constructors / setters / getters / toString } |
為了確保此嵌套對象的驗證,我們將使用@Valid注釋來裝飾該屬性:
1
2
3
4
5
6
7
8
9
10
|
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注解??????????????
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
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 + '}' ; } } |
比如,我們需要一次性保存多個UserAccount 對象,Controller層的方法可以這么寫:???????
1
2
3
4
5
6
7
8
9
|
@PostMapping ( "/saveList" ) public Result saveList( @RequestBody @Validated (UserAccount. class ) ValidationList<UserAccount > userList) { // 校驗通過,才會執行業務邏輯處理 return Result.ok(); } |
6. 自定義校驗
validator-api-2.0的約束注解有22個,具體我們看下面表格
空與非空檢查
注解 | 支持Java類型 | 說明 |
---|---|---|
@Null | Object | 為null |
@NotNull | Object | 不為null |
@NotBlank | CharSequence | 不為null,且必須有一個非空格字符 |
@NotEmpty | CharSequence、Collection、Map、Array | 不為null,且不為空(length/size>0) |
Boolean值檢查
注解 | 支持Java類型 | 說明 | 備注 |
---|---|---|---|
@AssertTrue | boolean、Boolean | 為true | 為null有效 |
@AssertFalse | boolean、Boolean | 為false | 為null有效 |
日期檢查
注解 | 支持Java類型 | 說明 | 備注 |
---|---|---|---|
@Future | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間之后 | 為null有效 |
@FutureOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間或之后 | 為null有效 |
@Past | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間之前 | 為null有效 |
@PastOrPresent | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate | 驗證日期為當前時間或之前 | 為null有效 |
數值檢查
注解 | 支持Java類型 | 說明 | 備注 |
---|---|---|---|
@Max | BigDecimal、BigInteger,byte、short、int、long以及包裝類 | 小于或等于 | 為null有效 |
@Min | BigDecimal、BigInteger,byte、short、int、long以及包裝類 | 大于或等于 | 為null有效 |
@DecimalMax | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包裝類 | 小于或等于 | 為null有效 |
@DecimalMin | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包裝類 | 大于或等于 | 為null有效 |
@Negative | BigDecimal、BigInteger,byte、short、int、long、float、double以及包裝類 | 負數 | 為null有效,0無效 |
@NegativeOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包裝類 | 負數或零 | 為null有效 |
@Positive | BigDecimal、BigInteger,byte、short、int、long、float、double以及包裝類 | 正數 | 為null有效,0無效 |
@PositiveOrZero | BigDecimal、BigInteger,byte、short、int、long、float、double以及包裝類 | 正數或零 | 為null有效 |
@Digits(integer = 3, fraction = 2) | BigDecimal、BigInteger、CharSequence,byte、short、int、long以及包裝類 | 整數位數和小數位數上限 | 為null有效 |
其他
注解 | 支持Java類型 | 說明 | 備注 |
---|---|---|---|
@Pattern | CharSequence | 匹配指定的正則表達式 | 為null有效 |
CharSequence | 郵箱地址 |
為null有效,默認正則 '.*' |
|
@Size | CharSequence、Collection、Map、Array | 大小范圍(length/size>0) | 為null有效 |
hibernate-validator擴展約束(部分)
注解 | 支持Java類型 | 說明 |
---|---|---|
@Length | String | 字符串長度范圍 |
@Range | 數值類型和String | 指定范圍 |
@URL | URL地址驗證 |
自定義約束注解
除了以上提供的約束注解(大部分情況都是能夠滿足的),我們還可以根據自己的需求自定義自己的約束注解
定義自定義約束,有三個步驟
- 創建約束注解
- 實現一個驗證器
- 定義默認的錯誤信息
那么下面就直接來定義一個簡單的驗證手機號碼的注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@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(); } } |
關于注解的配置這里不說了,自定義約束需要下面3個屬性
- message 錯誤提示信息,可以寫死,也可以填寫國際化的key
- groups 分組信息,允許指定此約束所屬的驗證組(下面會說到分組約束)
- payload 有效負載,可以通過payload來標記一些需要特殊處理的操作
@Repeatable注解和List定義可以讓該注解在同一個位置重復多次,通常是不同的配置(比如不同的分組和消息)
@Constraint(validatedBy = {MobileValidator.class})該注解是指明我們的自定義約束的驗證器,那下面就看一下驗證器的寫法,需要實現javax.validation.ConstraintValidator接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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(); } } |
ConstraintValidator接口定義了在實現中設置的兩個類型參數。
- 第一個指定要驗證的注解類(如Mobile),
- 第二個指定驗證器可以處理的元素類型(如String);initialize()方法可以訪問約束注解的屬性值;isValid()方法用于驗證,返回true表示驗證通過
Bean驗證規范建議將空值視為有效。如果null不是元素的有效值,則應使用@NotNull 顯式注釋
到這里我們自定義的約束就寫好了,可以用個例子來測試一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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的工作原理
方法級別參數校驗
在每個參數前面聲明約束注解,然后通過解析參數注解完成校驗,這就是方法級別的參數校驗。 這種方式可以用于任何的Spring Bean的方法上,一般來說,這種方式一般會采用AOP的Around增強完成 在Spring中,是通過以下步驟完成
- MethodValidationPostProcessor在Bean的初始化完成之后,判斷是否要進行AOP代理(類是否被@Validated標記)
- MethodValidationInterceptor攔截所有方法,執行校驗邏輯
- 委派Validator執行參數校驗和返回值校驗,得到ConstraintViolation
- 處理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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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 ; } } } } |
參考鏈接:
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
到此這篇關于Java中Validated、Valid 、Validator區別詳解的文章就介紹到這了,更多相關Validated、Valid 、Validator區別內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/fly910905/article/details/119850168