GroupSequence Provider and GroupSequence control the order of data validation and solve the problem of multi-field joint logic validation [Enjoy Spring MVC]

Every sentence

Don't complain about life and think about resigning every day like Sister Xianglin. The point of offence is to say, "degenerate" to work with such a person, is there no reason in oneself?

Preface

I thought I had said so much about Java/Spring data (binding) checking, which is basically over. But today at noon, a small enthusiastic partner encountered a slightly special case when using Bean Validation to do data validation. Because this validation scenario is more common, this paper has supplemented data validation.

With regard to data validation in Java/Spring, I have reason to believe that you must have met the scenario requirement that the validation logic of attribute b depends on the value of attribute a when validating JavaBean s; for another concrete example, the validation logic of attribute b takes effect only if and only if the value of attribute a equals xxx. This is what we often call multi-field joint verification logic.~
Because the case of this validation is relatively common, it motivates me to record this article because it will become meaningful and valuable. Of course, some of the small partners said that they could use if else to deal with this problem, it is not very troublesome. The purpose of this paper is still to strive for more refreshing, more elegant and better expansion of data validation consistently.

A little perseverance is needed: since Bean Validation is used to simplify validation, it's (best) not to use four different methods to solve problems.~

Description of enthusiastic netizens'questions

In order to restore the problem scenario more authentically, I paste up the chat screenshot as follows:

The request JavaBean to be verified is as follows:

The description of school needs is as follows:

This netizen describes the real production scenario problem, which is also the content of this article.
Although this is data validation under Spring MVC conditions, according to my habit, in order to explain the problem more conveniently, I will extract this part of the function list, explain the plan and principle, and then implement the problem itself (end of the article).~

Scheme and Principle

For single field validation, Cascade Property validation and so on, through reading my series of articles, I have reason to believe that small partners can be familiar with. This paper gives a simple example of "review":

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

	@NotNull
    @Size(min = 3, max = 5)
    private List<String> hobbies;

    // Cascade Calibration
    @Valid
    @NotNull
    private Child child;
}

Test:

public static void main(String[] args)  {
    Person person = new Person();
    person.setName("fsx");
    person.setAge(5);
	person.setHobbies(Arrays.asList("Football","Basketball"));
    person.setChild(new Child());

    Set<ConstraintViolation<Person>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(person);

    // Ergodic output of results
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

Run, print out:

child.name cannot be null: null
 age needs to be between 10 and 40: 5
 The number of hobbies must be between 3 and 5: [Football, basketball]

The results are in line with expectations, and the (cascade) validation takes effect.

Recursive validation can be achieved by using @Valid, so it can be tagged on a List to perform validation on every object in it.

Here comes the problem. For the above example, I now have the following requirements:

  1. If 20 <= age < 30, the size of hobbies should be between 1 and 2.
  2. If 30 <= age < 40, the size of hobbies should be between 3 and 5.
  3. The remaining values of age, hobbies without checking logic

Implementation scheme

Hibernate Validator provides a non-standard @GroupSequenceProvider annotation. This function provides a dynamic decision to load those check groups into the default check group according to the state of the current object instance.

To achieve the above requirements, we need to use the DefaultGroupSequenceProvider interface provided by Hibernate Validation to handle them.

// This interface defines the protocol of dynamic Group sequence.
// To make it work, you need to annotate @GroupSequenceProvider on T and specify this class as a processing class.
// If the `Default'group validates T, an instance of the actual validation is passed to this class to determine the default group sequence (this sentence is particularly important, as explained by an example below).
public interface DefaultGroupSequenceProvider<T> {
	// The eligible method is to return T to the default group (multiple). Because the default group is Default, ~~can be specified by itself.
	// Target allows dynamic combination of default group sequences in functions that validate the value state. (Very powerful)
	// object is a Bean to be verified. It can be null.

	// The return value represents the List of the default group sequence. It works the same way as @GroupSequence defines a sequence of groups, especially when list lists must contain type T
	List<Class<?>> getValidationGroups(T object);
}

Be careful:

  1. This interface Hibernate does not provide implementation
  2. If you implement it, you must provide an empty constructor and ensure thread safety.

Step by step to solve the logic of multi-field combination validation:
1. Implement DefaultGroupSequenceProvider Interface (Handling Person Bean)

public class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {

    @Override
    public List<Class<?>> getValidationGroups(Person bean) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(Person.class); // This step should not be saved, otherwise the Default group will not be executed and will be thrown wrong.

        if (bean != null) { // Make sure you do that.
            Integer age = bean.getAge();
            System.err.println("Age:" + age + ",Executing Corresponding Check Logic");
            if (age >= 20 && age < 30) {
                defaultGroupSequence.add(Person.WhenAge20And30Group.class);
            } else if (age >= 30 && age < 40) {
                defaultGroupSequence.add(Person.WhenAge30And40Group.class);
            }
        }
        return defaultGroupSequence;
    }
}

2. Use the @GroupSequenceProvider annotation to specify the processor in the java bean to be verified. And define the corresponding check logic (including grouping)

@GroupSequenceProvider(PersonGroupSequenceProvider.class)
@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Range(min = 10, max = 40)
    private Integer age;

    @NotNull(groups = {WhenAge20And30Group.class, WhenAge30And40Group.class})
    @Size(min = 1, max = 2, groups = WhenAge20And30Group.class)
    @Size(min = 3, max = 5, groups = WhenAge30And40Group.class)
    private List<String> hobbies;

    /**
     * Define exclusive business logic grouping
     */
    public interface WhenAge20And30Group {
    }
    public interface WhenAge30And40Group {
    }
}

Ibid., make simple modifications: person.setAge(25), run print output:

Age: 25, execute corresponding validation logic
 Age: 25, execute corresponding validation logic

No news of the failure of the validation (that is, good news) meets expectations.
Modify it to person.setAge(35) and run the print again as follows:

Age: 35, execute corresponding validation logic
 Age: 35, execute corresponding validation logic
 The number of hobbies must be between 3 and 5: [Football, basketball]

The verification is successful and the results are in line with expectations.
As you can see from this case, I have fully implemented the logic of multi-field combination validation through @GroupSequenceProvider, and the code is very elegant and extensible. I hope this example will help you.

The provider processor in Benefit is Person-specific, and of course you can use Object + reflection to make it more generic, but I don't recommend doing so on the principle of single responsibility.

Use the @GroupSequence annotation provided by JSR to control the order of validation

The above implementation is the best practice, easy to use, and very flexible. But we have to understand that it's a capability offered by Hibernate Validation, not by JSR standards.
@ GroupSequence is an annotation provided by the JSR standard (just not as powerful as the provider, but also very suitable for its use scenarios)

// Defines group sequence. Defines group sequence (sequence: sequential execution)
@Target({ TYPE })
@Retention(RUNTIME)
@Documented
public @interface GroupSequence {
	Class<?>[] value();
}

As the name implies, it represents a Group sequence. By default, constraint validation for different groups is out of order
In some cases, the order of constraint validation is very important, such as the following two scenarios:

  1. The constraint validation of the second group depends on the results of the first constraint execution (the first constraint must be correct, and the second constraint execution is meaningful)
  2. Verification of a Group is time-consuming and consumes a large amount of CPU / memory. So our approach should be to put this kind of verification at the end, so the order is required.

A group can be defined as a sequence of other groups, and its validation must conform to the sequence specified in that sequence. When using group sequence validation, if the group validation at the front of the sequence fails, the later group will no longer be validated.

Here's a chestnut:

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 {
	}
	// Group Sequence
	@GroupSequence({Default.class, GroupA.class, GroupB.class})
	public interface Group {
	}
}

Test:

public static void main(String[] args)  {
    User user = new User();
    // The verification group is specified here: User.Group.class
    Set<ConstraintViolation<User>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(user, User.Group.class);

    // Ergodic output of results
    result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

Running, console printing:

middlename middlename may be empty: null
firstname firstname may be empty: null

Phenomenon: Only Default Group checks, and other groups in the sequence do not perform the checks. Amend as follows:

        User user = new User();
        user.setFirstname("f");
        user.setMiddlename("s");

Running, console printing:

lastname lastname may be empty: null

Phenomenon: After the Default group has passed the validation, the group A validation is performed. But if the GroupA checking log passes, the GroupB checking will not be performed.~
@ GroupSequence provides sequential execution of group sequences and short-circuit capabilities, which are very useful in many scenarios.

For this example, it is relatively difficult to use @GroupSequence to complete multi-field combinational logic verification. But it is not impossible to do so. Here I offer some ideas for reference.

  1. Logic and "communication" between multiple fields are implemented through custom validation annotations at the class level (as for why it must be at the class level, don't explain ~)
  2. @ GroupSequence is used to control group execution order (let class-level custom annotations execute first)
  3. Adding a third attribute at the Bean level to assist in validation~

Of course, it is impossible to use it to solve the problem in practical application, so I don't need space here. I personally suggest that interested people can try it on their own, which will help deepen your understanding of data validation.

This article It has been said that data validation annotations can be annotated at the Field attribute, method, constructor, and Class class level. So we can control the order of checking them, not the inaccessibility of some articles on the internet.~

Note: The order can only be controlled at the grouping level, not at the constraint annotation level. Because the constraints in a class (within the same group) are guaranteed by Set < MetaConstraint <?> metaConstraints, it can be considered that the checkers in the same group are executed sequentially (whether classes, attributes, methods, constructors, etc.).

So there is a saying on the internet: the order of checking is to check field attributes first, which is not true in class level checking, please pay attention to distinguishing.

Principle analysis

In this paper, I use @GroupSequenceProvider to solve the pain point of multi-field combinational logic checking in normal development. Generally speaking, it is simple to use, and the code is modular enough to be easy to maintain.
But for the output of the above example, you may have at least the following questions as I do:

  1. Why do you have to say: defaultGroupSequence.add(Person.class)
  2. Why if (bean!= null) must be empty
  3. Why is the age: 35, the corresponding checking logic is output twice (there are two times in the blank), but the failure information of the checking only meets the expectation once?

With questions, I start with the validate verification implementation process.
1. Entry: ValidatorImpl. validate (T object, Class <?> groups)

ValidatorImpl: 
	@Override
	public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
		Class<T> rootBeanClass = (Class<T>) object.getClass();
		// Get BeanMetaData, information on classes: Group sequences on classes, default grouping lists for such classes, and so on
		BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
		...
	}

2. beanMetaDataManager.getBeanMetaData(rootBeanClass) obtains meta-information about the beans to be verified

Note that only Class is passed in here, not Object. That's why it's added!= Null's core reason for blanking (as you can see later, null is passed in).

BeanMetaDataManager:
	public <T> BeanMetaData<T> getBeanMetaData(Class<T> beanClass) {
		...
		// Annotation MetaData Provider is invoked to parse constrained annotation metadata information (and, of course, xml/Programmatic-based, outlined in this article) 
		// Note: It will recursively process parent classes, parent interfaces, etc. to get metadata for all classes.

		// The BeanMetaDataImpl.build() method does N things in the new BeanMetaDataImpl(...) constructor.
		// Among them are defaultGroupSequenceProvider related to my example
		beanMetaData = createBeanMetaData( beanClass );
	}

3. new BeanMetaDataImpl(...) constructs metadata information for this Class (Person.class in this case)

BeanMetaDataImpl: 
	public BeanMetaDataImpl(Class<T> beanClass,
							List<Class<?>> defaultGroupSequence, // If not configured, defaultGroupSequence is generally null at this time
							DefaultGroupSequenceProvider<? super T> defaultGroupSequenceProvider, // Our custom provider for handling this Bean
							Set<ConstraintMetaData> constraintMetaDataSet, // Contains all attributes, constructors, methods, and so on of the parent class. Here it will be categorized: classified according to attributes, methods, etc.
							ValidationOrderGenerator validationOrderGenerator) {
		... //Classification of constraintMetaDataSet
		// This method is to screen out: all constraints annotations (such as six constraints annotations, where the length is 6, of course, including fields, methods, etc.).
		this.directMetaConstraints = getDirectConstraints();

		// Because our Person class has defaultGroupSequenceProvider, we return true here
		// In addition to defining on the class, you can also define global: assign values to this field of the class List < Class <?> defaultGroupSequence
		boolean defaultGroupSequenceIsRedefined = defaultGroupSequenceIsRedefined();
		
		// That's why we have to judge the core of empty: see what it passes: null. So NPE is not empty. This is the first time that the defaultGroupSequenceProvider.getValidationGroups() method is called
		List<Class<?>> resolvedDefaultGroupSequence = getDefaultGroupSequence( null );
		... // After getting the resolvedDefaultGroup Sequence grouping information above, it will be put into all the validators (including attributes, methods, constructors, classes, etc.).
		// so, the default group sequence is still very important (note: the default group can have more than one oh ~)
	}


	@Override
	public List<Class<?>> getDefaultGroupSequence(T beanState) {
		if (hasDefaultGroupSequenceProvider()) {
			// so, in the getValidation Groups method, remember to empty the judgement~
			List<Class<?>> providerDefaultGroupSequence = defaultGroupSequenceProvider.getValidationGroups( beanState );
			// Most importantly, this method: getValidDefaultGroupSequence analyses default values~~~
			return getValidDefaultGroupSequence( beanClass, providerDefaultGroupSequence );
		}
		return defaultGroupSequence;
	}

	private static List<Class<?>> getValidDefaultGroupSequence(Class<?> beanClass, List<Class<?>> groupSequence) {
		List<Class<?>> validDefaultGroupSequence = new ArrayList<>();
		boolean groupSequenceContainsDefault = false; // Flag bit: If the Default group is not resolved, an exception is thrown

		// important
		if (groupSequence != null) {
			for ( Class<?> group : groupSequence ) {
				// That's why we need the sentence `defaultGroupSequence.add(Person.class)', because Default is needed to take effect.~~~
				if ( group.getName().equals( beanClass.getName() ) ) {
					validDefaultGroupSequence.add( Default.class );
					groupSequenceContainsDefault = true;
				} 
				// Meaning: You need to add the Default group and use the Class of this class instead of adding the Default.class.~
				else if ( group.getName().equals( Default.class.getName() ) ) { 
					throw LOG.getNoDefaultGroupInGroupSequenceException();
				} else { // Normally add to default group
					validDefaultGroupSequence.add( group );
				}
			}
		}
		// If the Default group is not found, an exception is thrown~
		if ( !groupSequenceContainsDefault ) {
			throw LOG.getBeanClassMustBePartOfRedefinedDefaultGroupSequenceException( beanClass );
		}
		return validDefaultGroupSequence;
	}

At this point, the defaultGroupSequenceProvider. getValidation Groups (null) is executed once (for the first time) just in the initial BeanMetaData phase, so it is necessary to make a blank judgement. It is also necessary to add this class to the default group (otherwise error is reported).~
Here the BeanMetaData < T > rootBeanMetaData is created, and the logic of validate() continues.~

4. determineGroup Validation Order (groups) determines the group sequence (execution order of groups) from the group specified by the caller

ValidatorImpl: 
	@Override
	public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
		...
		BeanMetaData<T> rootBeanMetaData = beanMetaDataManager.getBeanMetaData( rootBeanClass );
		...
		... // Prepare Validation Context (holding rootBeanMetaData and object instances)
		
		// groups are grouping arrays passed in by the caller (corresponding to the Group information specified in Spring MVC ~)
		ValidationOrder validationOrder = determineGroupValidationOrder(groups);
		... // Prepare ValueContext (holding rootBeanMetaData and object instances)

		// At this point, it's still at the Bean level, and it's time to start validating the bean.
		return validateInContext( validationContext, valueContext, validationOrder );
	}

	private ValidationOrder determineGroupValidationOrder(Class<?>[] groups) {
		Collection<Class<?>> resultGroups;
		// if no groups is specified use the default
		if ( groups.length == 0 ) {
			resultGroups = DEFAULT_GROUPS;
		} else {
			resultGroups = Arrays.asList( groups );
		}
		// getValidationOrder() is the main logical description. At this point resultGroups is at least a [Default.class]
		// 1. If it's just a Default.class, return it directly.
		// 2. Travel through all groups. (The specified Group must be an interface)
		// 3. If the traversed group is annotated with `GroupSequence', special processing of the sequence (adding the groups in the sequence)
		// 4. Ordinary Group, then new Group (clazz) is added to `validation Order'. And insert recursively (because there may be a parent interface)
		return validationOrderGenerator.getValidationOrder( resultGroups );
	}

To this Validation Order (actually DefaultValidation Order), the Groups passed in when the caller calls the validate() method are saved. The grouping sequence @GroupSequence is then parsed.
By validateInContext(...), you're starting to check this Bean with the Groups grouping and meta information.~

5. validateInContext(...) Checks this Bean in context (check context, value context, specified grouping)

ValidatorImpl: 
	private <T, U> Set<ConstraintViolation<T>> validateInContext(ValidationContext<T> validationContext, ValueContext<U, Object> valueContext, ValidationOrder validationOrder) {
		if ( valueContext.getCurrentBean() == null ) { // Compatible with null values for the entire Bean
			return Collections.emptySet();
		}
		// If the Bean is marked on the header (defaultGroupSequence processing is required), deal with it in a special way.
		// In this case, our Person must be true and can be accessed.
		BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
		if ( beanMetaData.defaultGroupSequenceIsRedefined() ) {

			// Note that the method beanMetaData.getDefaultGroupSequence() is called here again, which is a second call.
			// Object yo ~introduced here explains why `age: xxx'was printed twice in the judgement space.
			// assertDefaultGroupSequenceIsExpandable method is an empty method (by default) that can be ignored
			validationOrder.assertDefaultGroupSequenceIsExpandable( beanMetaData.getDefaultGroupSequence( valueContext.getCurrentBean() ) );
		}

		// ============== The following is important for execution order===============
		// validationOrder is loaded with groupings specified by the caller (parsing grouping sequences to ensure order ~)
		// Special attention should be paid to the fact that the designated grouping alone is out of order (there is no guarantee of the order of checking), so it is necessary to verify carefully if multiple groupings are designated.
		Iterator<Group> groupIterator = validationOrder.getGroupIterator();
		// Group validation is performed one by one according to the group (order) specified by the caller.
		while ( groupIterator.hasNext() ) {
			Group group = groupIterator.next();
			valueContext.setCurrentGroup(group.getDefiningClass()); // Setting the currently executing grouping

			// This step is slightly more complex and one of the core logic. The general process is as follows:
			// 1. Get the BeanMetaData of the Bean
			// 2. If defaultGroupSequenceIsRedefined()=true, the Person in this case annotates the provder annotation, so there is a specified grouping sequence
			// 3. Perform groupings one by one according to the order of the grouping sequence (all constrained MetaConstraint s perform groupings sequentially)
			// 4. Final completion of all MetaConstraint verification, and then complete the verification of all fields, methods and so on in this part.
			validateConstraintsForCurrentGroup( validationContext, valueContext );
			if ( shouldFailFast( validationContext ) ) {
				return validationContext.getFailingConstraints();
			}
		}
		
		... // Check validate Cascaded Constraints with the same code as above
		
		// Continue traversing the sequence: related to @GroupSequence
		Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
		...

		// Check context error message: It will put all the validator context Constraint Validator Context together under this check
		// Note: The context Constraint Validator Context between all validation annotations is completely independent and cannot access communication with each other.
		return validationContext.getFailingConstraints();
	}

that is all. At this point, the whole check is completed. If it does not fail quickly, by default, all the check failures will be received.

The real way to execute isValid is here:

public abstract class ConstraintTree<A extends Annotation> {
	...
	protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(
			ValidationContext<T> executionContext, // It knows the class it belongs to.
			ValueContext<?, ?> valueContext,
			ConstraintValidatorContextImpl constraintValidatorContext,
			ConstraintValidator<A, V> validator) {
			
		boolean isValid;
		// Resolve the value
		V validatedValue = (V) valueContext.getCurrentValidatedValue(); 
		// Give the value value to the isValid method of the verifier to verify.~~~
		isValid = validator.isValid(validatedValue,constraintValidatorContext);
		...
		if (!isValid) {
			// Use the constraintValidatorContext check context to generate error messages without checking
			// Context is used because, after all, there are more than one error message.~~~
			// Of course, with the help of the executionContext method ~~, the internal call is actually constraintValidatorContext.getConstraintViolationCreationContexts().
			return executionContext.createConstraintViolations(valueContext, constraintValidatorContext);
		}
	}
}

As for how the context Constraint Validator Context comes out, it's new: new Constraint Validator Context Impl (...), a check annotation for each field corresponds to a context (multiple constraint annotations can be tagged on an attribute), so this context is very isolated.

ValidationContext < T > ValidationContext and ValueContext <?, Object > ValueContext are class-level until the ValidatorImpl.validateMetaConstraints method begins a validation of the constraints~

Custom annotations only use the Constraint Validator Context context to the caller, but not to the validation Context and valueContext. I personally feel that this design is not flexible enough to easily achieve the effect of dependOn.~

Solving the Problem of Netizens

I put this part as the most important lead in this article to the end because I think my description has solved this kind of problem, not just this one.

Back to the question of enthusiastic netizens'response in the screenshot at the beginning of the article, as long as you read this article, I am very convinced that you have the means to use Bean Validation to solve elegantly. If you don't have any comments, I'll skip here.~

summary

This article describes how to use @GroupSequenceProvider to solve the problem of multi-field joint logic checking, which may be the pain point of many people's development. I hope this article can help you clear the obstacles before and embrace Bean Validation in an all-round way.~
I also convey a point in this article: I believe that popular open source stuff is excellent, not very extreme case, and deep use of it can solve most of your problems.

Relevant Reading

What's the difference between Spring@Validated and Valid? Teach you how to use it to complete Controller parameter checking (including Cascade Property checking) and principle analysis
Closing Bean Validation: You have to focus on the corners and edges (constraints cascade, custom constraints, custom validators, Internationalization Failure messages...)
[Xiaojia Java] Deep understanding of data validation: Java Bean Validation 2.0 (JSR303, JSR349, JSR380) Hibernate-Validation 6.x use case

Knowledge exchange

The last: If you think this is helpful to you, you might as well give a compliment. Of course, sharing your circle of friends so that more small partners can see it is also authorized by the author himself.~

If you are interested in technical content, you can join the wx group: Java Senior Engineer and Architect Group.
If the group two-dimensional code fails, Please add wx number: fsx641385712 (or scan the wx two-dimensional code below). And note: "java into the group" words, will be manually invited to join the group

Keywords: Java Attribute Bean Validation Spring

Added by Stanley90 on Tue, 20 Aug 2019 16:34:01 +0300