1、概览 {#1概览}
本文将带你了解 Spring 中 @Valid 和 @Validated 注解的用法和它们之间的区别。
在大多数应用中,验证用户输入是常见的功能。在 Java 生态系统中,一般使用 Java Standard Bean Validation API 来支持这一点,从 Spring 4.0 版本开始,它就与 Spring 完美集成。@Valid
和 @Validated
注解就源自于这个 Standard Bean API。
2、@Valid 和 @Validated 注解 {#2valid-和-validated-注解}
在 Spring 中,通常使用 JSR-303 的 @Valid
注解进行方法级验证,以及用于标记成员属性以进行验证。不过,该注解不支持分组验证。
分组(Group)可以帮助限制在验证过程中应用的约束条件。一个特定的使用案例是 UI 引导(UI wizards)。在第一步中,可能会有一组特定的字段。在后续步骤中,可能还有同一个 Bean 的另一组字段。因此,需要在每个步骤中对这些有限的字段应用约束条件,但是 @Valid
无法支持这样的功能。
对于分组级(Group-Level)验证,必须使用 Spring 的 @Validated
,它是 JSR-303 的 @Valid
的变体,用于方法级。对于成员属性的标记,继续使用 @Valid
注解就行。
3、示例 {#3示例}
使用 Spring Boot 开发一个简单的用户注册表单。
首先,只有 name
和 password
属性:
public class UserAccount {
@NotNull
@Size(min = 4, max = 15)
private String password;
@NotBlank
private String name;
// 构造函数、Get、Set 省略
}
接下来是 Controller。在 saveBasicInfo
方法参数上使用 @Valid
注解来验证用户输入:
@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());
}
确认测试运行成功后,扩展功能。下一个合理的步骤是将其转换为多步骤的注册表单,就像大多数引导应用一样。第一步中的 name
和 password
保持不变。在第二步中,获取额外的信息,例如 age
和 phone
。然后,使用这些额外的字段更新 Domain 对象:
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;
// 省略其他代码
}
不过,这次会发现之前的测试失败了,这是因为没有传入 age
和 phone
字段,而这两个字段未出现在 UI 上。为了支持这种行为,需要使用 @Validated
注解进行分组验证。
为此,需要将字段分组,创建两个不同的组。首先,需要创建两个标记接口(空接口,没有任何方法),每个组或每个步骤各一个。
第一步使用 BasicInfo
接口,第二步使用 AdvanceInfo
接口(接口的细节不是本地重点)。
还要更新 UserAccount
类,以使用这些标记接口:
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;
// 省略其他代码
}
更新 Controller,使用 @Validated
注解代替 @Valid
注解:
@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";
}
更新后,测试方法现在可以成功运行了。
再测试一个新方法:
@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
适用于这种需要分组验证的场景。
4、使用 @Valid 注解标记嵌套对象 {#4使用-valid-注解标记嵌套对象}
@Valid
注解用于标记嵌套属性。这会触发嵌套对象的验证。例如,在当前场景中,可以创建一个 UserAddress
对象:
public class UserAddress {
@NotBlank
private String countryCode;
// 其他代码省略
}
使用 @Valid
注解来装饰该属性,确保对嵌套对象进行验证:
public class UserAccount {
//...
@Valid
@NotNull(groups = AdvanceInfo.class)
private UserAddress useraddress;
// 其他代码省略
}
5、利弊 {#5利弊}
@Valid
注解用于对整个对象进行验证,但不能对字段进行分组验证。
@Validated
可以进行组验证。但是,在这种情况下,被验证的实体必须知道它们所使用的所有组或用例的验证规则。这混合了关注点,可能会导致反模式(在实践中经常出现但又低效或是有待优化的设计模式)。
6、总结 {#6总结}
本文介绍了 Spring 应用中 @Valid
注解和 @Validated
注解之间的主要区别。
总的来说:对于基本验证和嵌套属性验证,使用 JSR @Valid
注解。对于分组验证使用 Spring 的 @Validated
注解。
Ref:https://www.baeldung.com/spring-valid-vs-validated