2. @Valid和@Validated 注解在 Spring 中

简介: 2. @Valid和@Validated 注解在 Spring 中,我们使用 JSR-303 的@Valid注释进行方法级别验证。

@Valid和@Validated区别Validator2. @Valid和@Validated 注解3. 例子4.使用@Valid嵌套校验5. 组合使用@Valid和@Validated 进行集合校验6. 自定义校验空与非空检查Boolean值检查日期检查数值检查其他hibernate-validator扩展约束(部分)自定义约束注解工作原理 结论1. 结论先出Valid VS Validated 相同点 都可以对方法和参数进行校验 @Valid和@Validated 两种注释都会导致应用标准Bean验证。

如果验证不通过会抛出BindException异常,并变成400(BAD_REQUEST)响应;或者可以通过Errors或BindingResult参数在控制器内本地处理验证错误。

JSR 380 JSR 380 是用于 bean 验证的 Ja API 规范,是 Jakarta EE 和 JaSE 的一部分。

这确保 bean 的属性满足特定条件,使用诸如@NotNull、@Min和@Max 之类的注释。

此版本需要 Ja 8 或更高版本,并利用 Ja 8 中添加的新功能,例如类型注释和对Optional和LocalDate等新类型的支持。

有关规范的完整信息,请继续阅读JSR 380。

jax.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区别ValidatorBean Validation 2.0(JSR 380)定义了用于实体和方法验证的元数据模型和API,Hibernate Validator是目前最好的实现Validator接口有三个方法,可用于验证整个实体或仅验证实体的单个属性 Validator#validate() 验证所有bean的所有约束 Validator#validateProperty() 验证单个属性 Validator#validateValue() 检查给定类的单个属性是否可以成功验证 不管是requestBody参数校验还是方法级别的校验,最终都是调用Hibernate Validator执行校验,Spring Validation只是做了一层封装。

验证用户的输入是我们大多数应用程序中的常见功能。

在 Ja 生态系统中,我们专门使用Ja Standard Bean Validation API来支持这一点。

此外,从 4.0 版本开始,这也与 Spring 很好地集成在一起.在接下来的部分中,让我们详细了解它们。

2. @Valid和@Validated 注解在 Spring 中,我们使用 JSR-303 的@Valid注释进行方法级别验证。

一个特殊的用例是 UI 界面(UI wizards)。

在后续步骤中,可能有另一个组属于同一个 bean。

因此我们需要在每一步中对这些有限的字段应用约束,但@Valid不支持这一点。

在这种情况下,对于组级别,我们必须使用 Spring 的@Validated,它是 JSR-303 的@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注释的seBasicInfo方法来验证用户输入:@RequestMapping(value = "/seBasicInfo", method = RequestMethod.POST) public String seBasicInfo( @Valid @ModelAttribute("useraccount") UserAccount useraccount, BindingResult result, ModelMap model) { if (result.hasErrors()) { return "error"; } return "success"; }现在让我们测试这个方法:@Test public void givenSeBasicInfo_whenCorrectInput_thenSuccess() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders.post("/seBasicInfo") .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 } 但是,这一次我们会注意到之前的测试失败了。

我们将有第一步asicInfo接口和第二步的 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 = "/seBasicInfoStep1", method = RequestMethod.POST) public String seBasicInfoStep1( @Validated(BasicInfo.class) @ModelAttribute("useraccount") UserAccount useraccount, BindingResult result, ModelMap model) { if (result.hasErrors()) { return "error"; } return "success"; }由于此更新,我们的测试现在成功运行。

因此,我们可以看到@Validated的使用 对于组验证至关重要。

接下来,让我们看看@Valid如何触发嵌套属性的验证。

例如,在我们当前的场景中,让我们创建一个 UserAddress 对象:public class UserAddress { @NotBlank private String couyCode; // 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数组给,并希望对数组中的每一项都进行参数校验。

我们可以使用自定义list集合来接收参数:包装List类型,并声明@Valid注解 package com.devicemag.core.BO; import jax.validation.Valid;import ja.util.*; /** * @Title: 参数校验工具类, 用于校验List 类型的请求参数 * @ClassName: com.devicemag.core.BO.ValidList.ja * @Description: * * @Copyright 2020-2021 - Powered By 研发中心 * @author: 王延飞 * @date: 2020/12/25 20:23 * @version V1.0 */public class ValidList implements List { @Valid private List 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 iterator() { return list.iterator(); } @Override public Object[] toArray() { return list.toArray(); } @Override public 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 c) { return list.addAll(c); } @Override public boolean addAll(int index, Collection 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 listIterator() { return list.listIterator(); } @Override public ListIterator listIterator(int index) { return list.listIterator(index); } @Override public List subList(int fromIndex, int toIndex) { return list.subList(fromIndex, toIndex); } public List getList() { return list; } public void setList(List list) { this.list = list; } // 一定要记得重写toString方法 @Override public String toString() { return "ValidList{" + "list=" + list + '}'; }}比如,我们需要一次性保存多个UserAccount 对象,Cooller层的方法可以这么写:@PostMapping("/seList")public Result seList(@RequestBody @Validated(UserAccount.class) ValidationList userList) {// 校验通过,才会执行业务逻辑处理return Result.ok();}6. 自定义校验 validator-api-2.0的约束注解有22个,具体我们看下面表格 空与非空检查Boolean值检查日期检查数值检查其他hibernate-validator扩展约束(部分)自定义约束注解除了以上的约束注解(大部分情况都是能够满足的),我们还可以根据自己的需求自定义自己的约束注解定义自定义约束,有三个步骤 创建约束注解 实现一个验证器 定义默认的错误信息 那么下面就直接来定义一个简单的验证手机号码的注解@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[] 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})该注解是指明我们的自定义约束的验证器,那下面就看一下验证器的写法,需要实现jax.validation.ConstraintValidator接口 public class MobileValidator implements ConstraintValidator { /** * 手机验证规则 */ 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> 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 还需要注释来触发嵌套属性的验证。

在方法校验上,利用AOP动态拦截方法,利用JSR303 Validator实现完成校验。

在Bean的属性校验上,则是基于Bean的生命周期,在其初始化前后完成校验 Spring Validator本质实现还是JSR303 Validaotr,只是能让其更好的适配Spring Context @jax.validation.Valid是JSR303的核心标记注解,但是在Spring Framework中被@Validated取代,但是Spring Validator的实现可以支持兼容@jax.validation.Valid 例如,在MethodValidationPostProcessor了setValidatedAnnotationType方法,替换默认的@Validated在Spring MVC中,RequestResponseBodyMethodProcessor对@RequestBody和@ResponseBody的校验处理,就兼容了@jax.validation.Valid和@Validatedpublic 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-validatedhttps://docs.spring.io/spring-framework/docs/current/jadoc-api/org/springframework/validation/annotation/Validated.htmlhttps://docs.oracle.com/jaee/7/api/jax/validation/Valid.htmlhttps://docs.jboss.org/hibernate/beanvalidation/spec/2.0/api/jax/validation/Validator.htmlhttps://reflectoring.io/bean-validation-with-spring-boot/https://jcp.org/en/jsr/detail?id=380https://www.baeldung.com/jax-validation,https://blog.csdn.net/fly910905/article/details/119850168/反馈


以上是文章"

2. @Valid和@Validated 注解在 Spring 中

"的内容,欢迎阅读集优教育网的其它文章