目錄 #%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驗證。
如果驗證不通過會抛出
異常,并變成400(BAD_REQUEST)響應;或者可以通過
BindException
或
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
@Valid | @Validated | |
提供者 | JSR-303規範 | Spring |
是否支援分組 | 不支援 | 支援 |
标注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
嵌套校驗 |
Bean Validation 2.0(JSR 380)定義了用于實體和方法驗證的中繼資料模型和API,Hibernate Validator是目前最好的實作
Validator
接口有三個方法,可用于驗證整個實體或僅驗證明體的單個屬性
不管是
驗證所有bean的所有限制
Validator#validate()
驗證單個屬性
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 +
'}';
}
}
比如,我們需要一次性儲存多個
UserAccount
對象,
Controller
層的方法可以這麼寫:
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserAccount.class) ValidationList<UserAccount > userList) {
// 校驗通過,才會執行業務邏輯處理
return Result.ok();
}
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 | 比對指定的正規表達式 | ||
郵箱位址 | 為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();
}
}
關于注解的配置這裡不說了,自定義限制需要下面3個屬性
錯誤提示資訊,可以寫死,也可以填寫國際化的key
message
分組資訊,允許指定此限制所屬的驗證組(下面會說到分組限制)
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();
}
}
ConstraintValidator
接口定義了在實作中設定的兩個類型參數。
- 第一個指定要驗證的注解類(如
),Mobile
- 第二個指定驗證器可以處理的元素類型(如
);String
方法可以通路限制注解的屬性值;initialize()
方法用于驗證,傳回true表示驗證通過isValid()
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
的工作原理
方法級别參數校驗
在每個參數前面聲明限制注解,然後通過解析參數注解完成校驗,這就是方法級别的參數校驗。 這種方式可以用于任何的
Spring Bean
的方法上,一般來說,這種方式一般會采用
AOP
的
Around
增強完成 在Spring中,是通過以下步驟完成
-
在Bean的初始化完成之後,判斷是否要進行AOP代理(類是否被MethodValidationPostProcessor
标記)@Validated
-
攔截所有方法,執行校驗邏輯MethodValidationInterceptor
- 委派
執行參數校驗和傳回值校驗,得到Validator
ConstraintViolation
- 處理
ConstraintViolation
總之,對于任何基本驗證,我們将在方法調用中使用 JSR @Valid注釋。另一方面,對于任何組驗證,包括
組序列,我們需要 在我們的方法調用中使用 Spring 的@Validated注釋。所述@Valid 還需要注釋來觸發嵌套屬性的驗證。
-
的原理本質還是@Validated
。在方法校驗上,利用AOP動态攔截方法,利用AOP
實作完成校驗。在Bean的屬性校驗上,則是基于Bean的生命周期,在其初始化前後完成校驗JSR303 Validator
-
本質實作還是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;
}
}
}
}
參考連結:
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