Java中Validated、Valid 、Validator区别详解
目录
- 1. 结论先出
- JSR 380
- Valid VS Validated 不同点?
- Validator
- 2. @Valid和@Validated 注解
- 3. 例子
- 4.使用@Valid嵌套校验
- 5. 组合使用@Valid和@Validated 进行集合校验
- 6. 自定义校验
- 自定义约束注解
- 工作原理
- 结论
- 参考链接:
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 开发的简单用户注册。首先,我们将只有名称和密码属性:
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 { @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类以使用这些标记接口,如下所示:
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:
@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的使用 对于组验证至关重要。
接下来,让我们看看@Valid如何触发嵌套属性的验证。
4.使用@Valid嵌套校验
@Valid注释用于校验嵌套属性。这会触发嵌套对象的验证。例如,在我们当前的场景中,让我们创建一个 UserAddress 对象:
public class UserAddress { @NotBlank private String countryCode; // standard constructors / setters / getters / toString }
为了确保此嵌套对象的验证,我们将使用@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(); }
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地址验证 |
自定义约束注解
除了以上提供的约束注解(大部分情况都是能够满足的),我们还可以根据自己的需求自定义自己的约束注解
定义自定义约束,有三个步骤
- 创建约束注解
- 实现一个验证器
- 定义默认的错误信息
那么下面就直接来定义一个简单的验证手机号码的注解
@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接口
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 显式注释
到这里我们自定义的约束就写好了,可以用个例子来测试一下
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
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.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区别内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!