# validation-demo **Repository Path**: cckevincyh/validation-demo ## Basic Information - **Project Name**: validation-demo - **Description**: JAVA Bean Validation - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2020-09-10 - **Last Updated**: 2024-11-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] ## JAVA Bean Validation Bean Validation 顾名思义是对 java Bean 的校验,目前为止,Java 对 Bean 的校验有3个规范。 - JSR-303 : Bean Validation - JSR 349 : Bean Validation 1.1 - JSR 380 : Bean Validation 2.0 JSR是`Java Specification Requests`的缩写,意思是Java 规范提案。关于数据校验这块,最新的是`JSR380`,也就是我们常说的`Bean Validation 2.0`。 参考: [Spring 参数校验详解](https://juejin.im/post/6844904003310977031) [深入了解数据校验:Java Bean Validation 2.0(JSR380)](https://juejin.im/post/6844903895613833229) ## javax.validation `javax.validation` 是 Java 官方提供的对 **JAVA Bean Validation** 的规范,并没有提供实现。 由于 Bean Validation 有多个版本,因此 javax.validation 也提供了对于版本的实现 其中 - **1.0.x** 对应 **JSR-303 : Bean Validation** - **1.1.x** 对应 **JSR 349 : Bean Validation 1.1** - **2.0.x** 对应 **JSR 380 : Bean Validation 2.0** ```XML javax.validation validation-api 2.0.1.Final ``` ![](./img/Snipaste_2020-09-10_22-35-34.png) ## Hibernate Validator Bean Validation的实现,基于 **javax.validation**,并在它的基础上提供了更多的注解功能 ```XML org.hibernate hibernate-validator 6.1.5.Final ``` 如果你只引入了 **javax.validation**,而没有引入**Hibernate Validator**,就会报如下的错误: ``` javax.validation.NoProviderFoundException: Unable to create a Configuration, because no Bean Validation provider could be found. Add a provider like Hibernate Validator (RI) to your classpath. ``` 当我们引入这两个依赖之后 ```XML javax.validation validation-api 2.0.1.Final org.hibernate hibernate-validator 6.1.5.Final ``` 我们写一个代码进行测试,首先我们来定义一个Bean ```java package org.example; import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @Data @Builder(toBuilder = true) public class Foo { @NotBlank private String name; @NotNull private Integer age; } ``` 之后是测试类 ```JAVA package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import java.util.Set; public class TestFoo { @Test public void test01() { Foo foo = Foo.builder().build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(foo); System.out.println(validate); } } ``` 结果运行报错了: ``` javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead ``` 查看文档之后,发现还需要加入如下的依赖: ```xml org.glassfish jakarta.el 3.0.3 ``` 再次运行我们就可以看到控制台打印出了内容了: ``` [ConstraintViolationImpl{interpolatedMessage='不能为空', propertyPath=name, rootBeanClass=class org.example.Foo, messageTemplate='{javax.validation.constraints.NotBlank.message}'}, ConstraintViolationImpl{interpolatedMessage='不能为null', propertyPath=age, rootBeanClass=class org.example.Foo, messageTemplate='{javax.validation.constraints.NotNull.message}'}] ``` 参考: https://hibernate.org/validator/documentation/ [Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/) ## 验证模式 - 快速失败(fail fast) 当发生第一个验证失败时就立即结束。 ```java @Test public void testBeanPropertyValidationFailFast() { Foo foo = Foo.builder().build(); Validator validator = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory() .getValidator(); Set> validate = validator.validateProperty(foo, "name"); for (ConstraintViolation fooConstraintViolation : validate) { // property:name,error message:must not be blank System.out.println(MessageFormat.format("property:{0},error message:{1}", fooConstraintViolation.getPropertyPath().toString(), fooConstraintViolation.getMessage())); } } ``` - 非快速失败模式(默认) 收集所有失败再结束 ```java @Test public void testBeanPropertyValidation() { Foo foo = Foo.builder().build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validateProperty(foo, "name"); for (ConstraintViolation fooConstraintViolation : validate) { // property:name,error message:must not be null // property:name,error message:must not be blank System.out.println(MessageFormat.format("property:{0},error message:{1}", fooConstraintViolation.getPropertyPath().toString(), fooConstraintViolation.getMessage())); } } ``` ## 自定义验证消息 ```java @Data @Builder(toBuilder = true) public class Foo { @NotBlank @NotNull @Size(min = 2, max = 14, message = "name:[${validatedValue}],length: {min} to {max}" ) private String name; @NotNull private Integer age; } ``` - 被`{}`包围的成为消息参数 - 被`${}`包围的称为消息表达式 默认的消息定义如下: ![](./img/Snipaste_2020-09-11_09-58-34.png) ## 约束组(Group) 每个约束都至少要属于一个组,没有指定则属于默认(javax.validation.groups.**Default**)组。不分配groups,默认每次都要进行验证。如果指定则不再属于默认组。 ```java /* * Bean Validation API * * License: Apache License, Version 2.0 * See the license.txt file in the root directory or . */ package javax.validation.groups; /** * Default Bean Validation group. *

* Unless a list of groups is explicitly defined: *

    *
  • constraints belong to the {@code Default} group
  • *
  • validation applies to the {@code Default} group
  • *
* Most structural constraints should belong to the default group. * * @author Emmanuel Bernard */ public interface Default { } ``` 把约束分组可以让我们在对bean进行验证时可以更灵活。 比如:同样一个bean,在新增和修改两个业务场景中需要验证的属性是不一样的(如新增不需要验证id,修改时则需要) ```java package org.example; import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; @Data @Builder(toBuilder = true) public class Student { @NotNull(groups = ModGroup.class) public String no; @NotEmpty(groups = {AddGroup.class, ModGroup.class}) public String name; } ``` ```java package org.example; public interface AddGroup { } ``` ```java package org.example; public interface ModGroup { } ``` ```java package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import java.text.MessageFormat; import java.util.Set; public class TestStudent { @Test public void testBeanValidationGroup() { Student student = Student.builder().build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(student, AddGroup.class); for (ConstraintViolation studentConstraintViolation : validate) { // property:name,error message:must not be empty System.out.println(MessageFormat.format("property:{0},error message:{1}", studentConstraintViolation.getPropertyPath().toString(), studentConstraintViolation.getMessage())); } } } ``` 当然还可以指定多个组 ```java Validation.buildDefaultValidatorFactory().getValidator().validate(student, AddGroup.class,ModGroup.class); ``` 如果设置了groups,但是校验的时候不指定groups,则默认检查Default组,所以下面这个代码不会检测出任何的error ```java @Test public void testBeanValidationGroup() { Student student = Student.builder().build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(student); for (ConstraintViolation studentConstraintViolation : validate) { System.out.println(MessageFormat.format("property:{0},error message:{1}", studentConstraintViolation.getPropertyPath().toString(), studentConstraintViolation.getMessage())); } } ``` ## `@GroupSequence`组序列 `@GroupSequence`它是`JSR`标准提供的注解 顾名思义,它表示`Group组序列`。**默认情况下,不同组别的约束验证是无序的** 在某些情况下,约束验证的**顺序**是非常的重要的,比如如下两个场景: 1. 第二个**组**的约束验证依赖于第一个约束执行完成的结果(必须第一个约束正确了,第二个约束执行才有意义) 2. 某个Group组的校验非常耗时,并且会消耗比较大的CPU/内存。那么我们的做法应该是把这种校验**放到最后**,所以对顺序提出了要求 **一个组可以定义为其他组的序列**,使用它进行验证的时候必须符合该序列规定的顺序。`在使用组序列验证的时候`,**如果序列前边的组验证失败**,则后面的组将不再给予验证。 ```java package org.example; import lombok.Builder; import lombok.Data; import javax.validation.GroupSequence; import javax.validation.constraints.NotEmpty; import javax.validation.groups.Default; @Data @Builder(toBuilder = true) public class User { @NotEmpty(message = "firstName may be empty") private String firstName; @NotEmpty(message = "middleName may be empty", groups = Default.class) private String middleName; @NotEmpty(message = "lastName may be empty", groups = GroupA.class) private String lastName; @NotEmpty(message = "country may be empty", groups = GroupB.class) private String country; public interface GroupA { } public interface GroupB { } // 组序列 @GroupSequence({Default.class, GroupA.class, GroupB.class}) public interface Group { } } ``` ```java package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import java.text.MessageFormat; import java.util.Set; public class TestUser { @Test public void testGroupSequence() { User user = User.builder().build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class); // property: middleName, error message: middleName may be empty, invalid value: null // property: firstName, error message: firstName may be empty, invalid value: null validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } @Test public void testGroupSequence2() { User user = User.builder().firstName("k").middleName("c").build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class); // property: lastName, error message: lastName may be empty, invalid value: null validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } @Test public void testGroupSequence3() { User user = User.builder().firstName("k").middleName("c").lastName("g").build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class); // property: country, error message: country may be empty, invalid value: null validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } } ``` ## 级联校验 通过使用`@Valid`可以实现递归验证,因此可以标注在`List`上,对它里面的每个对象都执行校验 ```java package org.example; import lombok.Builder; import lombok.Data; import org.hibernate.validator.constraints.Range; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.List; @Data @Builder(toBuilder = true) public class Person { @NotNull private String name; @NotNull @Range(min = 10, max = 40) private Integer age; @Valid @NotNull @Size(min = 3, max = 5) private List childList; @Valid @NotNull private Child child; } @Data @Builder(toBuilder = true) class Child { @NotNull private String name; } ``` ```java package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Set; public class TestPerson { @Test public void testPersonValidation() { Person person = Person.builder() .child(Child.builder().build()) .childList(new ArrayList() {{ add(Child.builder().build()); }}).build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(person); // property: child.name, error message: must not be null, invalid value: null // property: age, error message: must not be null, invalid value: null // property: name, error message: must not be null, invalid value: null // property: childList, error message: size must be between 3 and 5, invalid value: [Child(name=null)] // property: childList[0].name, error message: must not be null, invalid value: null validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } } ``` ## 方法约束声明和验证,ExecutableValidator 从Bean Validation 1.1开始,约束不仅可以应用于JavaBean及其属性,而且可以应用于任何Java类型的方法和构造函数的参数和返回值 ```java package org.example; import lombok.Builder; import lombok.Data; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @Data @Builder(toBuilder = true) public class Teacher { private String name; public Teacher(@NotNull String name) { this.name = name; } public void teach(@NotBlank String content) { System.out.println(content); } public @Size(min = 5, max = 10) String getTeacherName(String name) { return name; } } ``` ```java package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.executable.ExecutableValidator; import java.text.MessageFormat; import java.util.Set; public class TestTeacher { @Test public void testConstructorValidation() throws NoSuchMethodException { ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables(); Set> constraintViolations = executableValidator.validateConstructorParameters(Teacher.class.getConstructor(String.class), new Object[]{null}); // property: Teacher.arg0, error message: must not be null, invalid value: null constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } @Test public void testMethodValidation() throws NoSuchMethodException { Teacher teacher = Teacher.builder().build(); ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables(); Set> constraintViolations = executableValidator.validateParameters(teacher, Teacher.class.getMethod("teach", String.class), new Object[]{null}); // property: teach.arg0, error message: must not be blank, invalid value: null constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } @Test public void testReturnValueValidation() throws NoSuchMethodException { Teacher teacher = Teacher.builder().build(); ExecutableValidator executableValidator = Validation.buildDefaultValidatorFactory().getValidator().forExecutables(); Set> constraintViolations = executableValidator.validateReturnValue(teacher, Teacher.class.getMethod("getTeacherName", String.class), "CC"); // property: getTeacherName., error message: size must be between 5 and 10, invalid value: CC constraintViolations.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } } ``` ## 自定义约束 1.创建约束注解 ```java package org.example; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = UpperCaseValidator.class) public @interface UpperCase { String message() default "must upper"; Class[] groups() default {}; Class[] payload() default {}; } ``` 2.编写验证器(Implement ConstraintValidator) ```java package org.example; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class UpperCaseValidator implements ConstraintValidator { @Override public void initialize(UpperCase constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { if (value == null) { return true; } return value.equals(value.toUpperCase()); } } ``` ```java package org.example; import org.junit.jupiter.api.Test; import javax.validation.ConstraintViolation; import javax.validation.Validation; import java.text.MessageFormat; import java.util.Set; public class TestEngineer { @Test public void testConstraintValidator() { Engineer engineer = Engineer.builder().name("test").build(); Set> validate = Validation.buildDefaultValidatorFactory().getValidator().validate(engineer); // property: name, error message: must upper, invalid value: test validate.stream().map(v -> MessageFormat.format("property: {0}, error message: {1}, invalid value: {2}", v.getPropertyPath().toString(), v.getMessage(), v.getInvalidValue())).forEach(System.out::println); } } ``` ## 参考 [参数校验 Hibernate-Validator](https://juejin.im/post/6844903961581846535) https://hibernate.org/validator/documentation/ [Hibernate Validator 6.1.5.Final - Jakarta Bean Validation Reference Implementation: Reference Guide](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/) [Spring 参数校验详解](https://juejin.im/post/6844904003310977031) [深入了解数据校验:Java Bean Validation 2.0(JSR380)](https://juejin.im/post/6844903895613833229) [分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题](https://cloud.tencent.com/developer/article/1497756) [Bean Validation和Hibernate Validator](https://www.jianshu.com/p/e4c99dc9df5a) [使用spring validation完成数据后端校验](https://blog.csdn.net/u013815546/article/details/77248003) [@Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated](https://blog.csdn.net/qq_27680317/article/details/79970590) [如何优雅的做数据校验-Hibernate Validator详细使用说明](https://juejin.im/post/6844903976270299149) [Spring 参数校验的异常处理](https://juejin.im/post/6844904003684302861) [使用spring validation完成数据后端校验](https://blog.csdn.net/u013815546/article/details/77248003)