SoFunction
Updated on 2025-04-05

Comprehensive analysis of SpringBoot parameter verification and principle

Preface

During the normal server development process, it is inevitable to verify the interface parameters. More common ones include the user name that cannot be empty, the age must be greater than 0, the email format must be compliant, etc.

If the parameters are checked through if else, the verification code will be coupled with the business and it will appear very verbose.

SpringBoot provides a concise and efficient way to perform parameter verification through @Validated/@Valid annotation, greatly improving work efficiency

1. Basic usage

There are three ways in total:

  • Controller's @RequestBody parameter verification
  • Controller's @RequestParam/@PathVariable parameter verification
  • Programmatic verification, directly call the validate method of hibernate

All three methods require the following dependencies. There are required -api and hibernate-validator packages inside

<dependency>
    <groupId></groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

@RequestBody parameter verification

This method is suitable for parameter verification of POST/PUT method in Controller. MethodArgumentNotValidException will be thrown if the verification fails.

1. First declare constraint annotations on the attributes of the parameter class, such as @NotBlank, @Email, etc.

@Data
public class UserVo implements Serializable {
    @NotBlank(message = "The name cannot be empty")
    @Size(min = 2, max = 50, message = "The length of the name range is 2~50")
    private String name;

    @Email(message = "The email format is wrong")
    private String email;

    @NotNull(message = "Age cannot be empty")
    @Min(18)
    @Max(100)
    private Integer age;

    @NotEmpty(message = "The photo cannot be empty")
    private List&lt;String&gt; photoList;
}

2. Then add the @Validated annotation next to the Controller method @RequestBody

@Slf4j
@RestController
public class UserController {
    @ApiOperation("Save User")
    @PostMapping("/save/user")
    public Result&lt;Boolean&gt; saveUser(@RequestBody @Validated UserVo user) {
        return ();
    }
}

@RequestParam/@PathVariable parameter verification

This method is suitable for parameter verification of GET method in Controller. If the verification fails, a ConstraintViolationException will be thrown. It is achieved by adding @Validated annotation to the class and adding @NotBlank and other constraint annotations before the method parameters, so other Spring Bean methods are also applicable

Add @Validated annotation to the class; add @NotBlank, @Max and other annotations next to @RequestParam/@PathVariable

@Slf4j
@RestController
@Validated
public class UserController {
    @ApiOperation("Query User")
    @GetMapping("/list/user")
    public Result&lt;List&lt;UserVo&gt;&gt; listUser(
            @Min(value = 100, message = "id cannot be less than 100") @RequestParam("id") Long id,
            @NotBlank(message = "The name cannot be empty") @RequestParam("name") String name,
            @Max(value = 90, message = "Age cannot be greater than 90") @RequestParam("age") Integer age) {
        List&lt;UserVo&gt; list = new ArrayList&lt;&gt;();
        return (list);
    }
}

Programming verification

This method is suitable for the verification of Service parameters. If the verification fails, the ValidationException will be thrown manually.

1. Initialize the Validator object through @bean annotation

public class ValidatorConfig {
    @Bean
    public Validator validator() {
        return ()
                .configure()
                // Fast failure mode                .failFast(true)
                .buildValidatorFactory()
                .getValidator();
    }
}

2. Call hibernate's validate method in the Service method to verify the parameters

@Service
@Slf4j
public class UserService {
    @Autowired
    private Validator validator;

    public boolean editUser(UserVo user) {
        Set<ConstraintViolation<UserVo>> validateSet = (user);
        if ((validateSet)) {
            StringBuilder errorMessage = new StringBuilder();
            for (ConstraintViolation<UserVo> violation : validateSet) {
                ("[").append(().toString()).append("]")
                        .append(()).append(";");
            }
            throw new ValidationException(());
        }
        return ;
    }
}

2. Advanced usage

Custom verification annotations

Some scenarios may not support built-in annotations in the -api and hibernate-validator packages. For example, when adding a user, you need to verify whether the user name is duplicated. At this time, you can implement it through custom annotations.

1. First customize the annotations

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Repeatable()
@Constraint(validatedBy = {})
public @interface UniqueName {
    String message() default "Username duplicate";
    // Grouping    Class&lt;?&gt;[] groups() default {};
    
    Class&lt;? extends Payload&gt;[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        UniqueName[] value();
    }
}

2. Then add a validator to the custom annotation

  • Implement the ConstraintValidator interface and specify the custom annotation <UniqueName> and the validated data type <String>
  • Rewrite the isValid method to implement verification logic
@Component
public class UniqueNameValidator implements ConstraintValidator<UniqueName, String> {

    @Autowired
    private UserService userService;

    @Override
    public boolean isValid(String name, ConstraintValidatorContext context) {
        if ((name)) {
            return true;
        }
        UserVo user = (name);
        if (user == null) {
            return true;
        }
        return false;
    }
}

3. Use custom annotations

@Data
public class UserVo implements Serializable {
    @UniqueName
    private String name;
}

Multi-attribute joint verification

whenOne field's verification depends on the value of another fieldWhen using multi-attribute joint verification, or group verification, it is necessary to use.

For example, a certain system needs to verify when submitting user information. When the gender is female, the photo information cannot be empty. At this time, whether the photo information can be empty depends on the gender value.

hibernate-validator provides the DefaultGroupSequenceProvider interface for us to customize grouping, and the specific use is as follows:

1. First define two groups, Boy and Girl

public interface Boy {
}

public interface Girl {
}

2. Grouping logic implementation. When the gender is female, the user is assigned to the Girl group.

public class CustomGroupSequenceProvider implements DefaultGroupSequenceProvider&lt;UserVo&gt; {

    @Override
    public List&lt;Class&lt;?&gt;&gt; getValidationGroups(UserVo user) {
        List&lt;Class&lt;?&gt;&gt; defaultGroupSequence = new ArrayList&lt;&gt;();
        ();
        if (user != null) {
            String sex = ();
            if ("female".equals(sex)) {
                ();
            }
        }
        return defaultGroupSequence;
    }
}

3. Use grouping to verify the photoList field

  • Add @GroupSequenceProvider() annotation to the entity class
  • Add @NotEmpty(message = "The photo cannot be empty when the gender is female", groups = {}) annotation
@Data
@GroupSequenceProvider()
public class UserVo implements Serializable {
    @NotBlank(message = "Gender cannot be empty")
    private String sex;

    @NotEmpty(message = "Photoes cannot be empty when a gender is female", groups = {})
    private List&lt;String&gt; photoList;
}

Nested verification

When there are object properties in VO objects that require verification, nested verification can be used.

1. Add @Valid annotation to object properties

@Data
public class UserVo implements Serializable {
    @Valid
    @NotNull(message = "The address cannot be empty")
    private Address address;
}

2. Then declare constraint annotations in the embedded object

@Data
public class Address implements Serializable {
    @NotBlank(message = "The address name cannot be empty")
    private String name;

    private String longitude;

    private String latitude;

}

Implementation principle

@RequestBody parameter verification implementation principle

All parameters annotated by @RequestBody must be processed by the RequestResponseBodyMethodProcessor class. This class is mainly used to parse the parameters of the @RequestBody annotation method and to process the return value of the @ResponseBody annotation method. Among them, resolveArgument() method is the entry to parse the @RequestBody annotation parameter

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = ();
        Object arg = readWithMessageConverters(webRequest, parameter, ());
        String name = (parameter);

        if (binderFactory != null) {
            WebDataBinder binder = (webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, ());
                }
            }
            if (mavContainer != null) {
                (BindingResult.MODEL_KEY_PREFIX + name, ());
            }
        }

        return adaptArgumentIfNecessary(arg, parameter);
    }
}

validateIfApplicable(binder, parameter) in resolveArgument method will verify parameters with @valid/@validate annotation

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = ();
		for (Annotation ann : annotations) {
			Object[] validationHints = (ann);
			if (validationHints != null) {
				(validationHints);
				break;
			}
		}
	}

//The @Validated annotation or annotation starting with @Valid will be checked
public static Object[] determineValidationHints(Annotation ann) {
		Class&lt;? extends Annotation&gt; annotationType = ();
		String annotationName = ();
		if ("".equals(annotationName)) {
			return EMPTY_OBJECT_ARRAY;
		}
		Validated validatedAnn = (ann, );
		if (validatedAnn != null) {
			Object hints = ();
			return convertValidationHints(hints);
		}
		if (().startsWith("Valid")) {
			Object hints = (ann);
			return convertValidationHints(hints);
		}
		return null;
	}

After Spring passes a one-turn adaptation conversion, it will put the parameter verification logic into the hibernate-validator and perform verification in ValidatorImpl#validate(T object, Class<?>... groups)

public class ValidatorImpl implements Validator, ExecutableValidator {

    @Override
    public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
        ( object, () );
        sanityCheckGroups( groups );

        @SuppressWarnings("unchecked")
        Class<T> rootBeanClass = (Class<T>) ();
        BeanMetaData<T> rootBeanMetaData = ( rootBeanClass );

        if ( !() ) {
            return ();
        }

        BaseBeanValidationContext<T>
                validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );

        ValidationOrder validationOrder = determineGroupValidationOrder( groups );
        BeanValueContext<?, Object> valueContext = (
                (),
                object,
                (),
                ()
        );

        return validateInContext( validationContext, valueContext, validationOrder );
    }
    
}

The specific verification process is in the validateConstraintsForSingleDefaultGroupElement method. It will traverse the constraint annotations such as @NotNull, @NotBlank, and @Email to see if the parameters meet the limits.

public class ValidatorImpl implements Validator, ExecutableValidator {

    private &lt;U&gt; boolean validateConstraintsForSingleDefaultGroupElement(BaseBeanValidationContext&lt;?&gt; validationContext, ValueContext&lt;U, Object&gt; valueContext, final Map&lt;Class&lt;?&gt;, Class&lt;?&gt;&gt; validatedInterfaces,
                                                                        Class&lt;? super U&gt; clazz, Set&lt;MetaConstraint&lt;?&gt;&gt; metaConstraints, Group defaultSequenceMember) {
        boolean validationSuccessful = true;

        ( () );
        //metaConstraints is a collection of constraint annotations such as @NotNull, @NotBlank, and @Email, verify one by one        for ( MetaConstraint&lt;?&gt; metaConstraint : metaConstraints ) {
            final Class&lt;?&gt; declaringClass = ().getDeclaringClass();
            if ( () ) {
                Class&lt;?&gt; validatedForClass = ( declaringClass );
                if ( validatedForClass != null &amp;&amp; !( clazz ) ) {
                    continue;
                }
                ( declaringClass, clazz );
            }

            boolean tmp = validateMetaConstraint( validationContext, valueContext, (), metaConstraint );
            if ( shouldFailFast( validationContext ) ) {
                return false;
            }

            validationSuccessful = validationSuccessful &amp;&amp; tmp;
        }
        return validationSuccessful;
    }
}

() is the entry to all validators, including built-in hibernate-validator, as well as custom ones

public abstract class ConstraintTree<A extends Annotation> {

    protected final <V> Optional<ConstraintValidatorContextImpl> validateSingleConstraint(
            ValueContext<?, ?> valueContext,
            ConstraintValidatorContextImpl constraintValidatorContext,
            ConstraintValidator<A, V> validator) {
        boolean isValid;
        try {
            @SuppressWarnings("unchecked")
            V validatedValue = (V) ();
            isValid = ( validatedValue, constraintValidatorContext );
        }
        catch (RuntimeException e) {
            if ( e instanceof ConstraintDeclarationException ) {
                throw e;
            }
            throw ( e );
        }
        if ( !isValid ) {
            //We do not add these violations yet, since we don't know how they are
            //going to influence the final boolean evaluation
            return ( constraintValidatorContext );
        }
        return ();
    }

}

The following is the specific implementation of @NotBlank constraint annotation validator

public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {

	/**
	 * Checks that the character sequence is not {@code null} nor empty after removing any leading or trailing
	 * whitespace.
	 *
	 * @param charSequence the character sequence to validate
	 * @param constraintValidatorContext context in which the constraint is evaluated
	 * @return returns {@code true} if the string is not {@code null} and the length of the trimmed
	 * {@code charSequence} is strictly superior to 0, {@code false} otherwise
	 */
	@Override
	public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
		if ( charSequence == null ) {
			return false;
		}

		return ().trim().length() > 0;
	}
}

@RequestParam/@PathVariable parameter verification implementation principle

This method is essentially achieved by adding @Validated annotation to the class and adding @NotBlank and other constraint annotations before the method parameters. The underlying layer uses Spring AOP, specifically, dynamically registering AOP sections through MethodValidationPostProcessor, and then using MethodValidationInterceptor to weave the point-cutting method to enhance it.

Here are the @Validated tangents initialized at container startup, as well as MethodValidationInterceptor enhancements

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
        implements InitializingBean {
    private Class<? extends Annotation> validatedAnnotationType = ;

    @Nullable
    private Validator validator;

    @Override
    public void afterPropertiesSet() {
        Pointcut pointcut = new AnnotationMatchingPointcut(, true);
         = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice());
    }
    
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

Specific enhancement logic is in MethodValidationInterceptor

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // Avoid Validator invocation on /isSingleton
        if (isFactoryBeanMetadataMethod(())) {
            return ();
        }

        Class<?>[] groups = determineValidationGroups(invocation);

        // Standard Bean Validation 1.1 API
        ExecutableValidator execVal = ();
        Method methodToValidate = ();
        Set<ConstraintViolation<Object>> result;

        try {
            result = (
                    (), methodToValidate, (), groups);
        }
        catch (IllegalArgumentException ex) {
            // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
            // Let's try to find the bridged method on the implementation class...
            methodToValidate = (
                    ((), ().getClass()));
            result = (
                    (), methodToValidate, (), groups);
        }
        if (!()) {
            throw new ConstraintViolationException(result);
        }

        Object returnValue = ();

        result = ((), methodToValidate, returnValue, groups);
        if (!()) {
            throw new ConstraintViolationException(result);
        }

        return returnValue;
    }
}

The () method is used for parameter verification and will eventually enter the hibernate-validator. The logic behind is similar to the above, so I will not repeat it here

public class ValidatorImpl implements Validator, ExecutableValidator {
    @Override
	public <T> Set<ConstraintViolation<T>> validateParameters(T object, Method method, Object[] parameterValues, Class<?>... groups) {
		( object, () );
		( method, () );
		( parameterValues, () );

		return validateParameters( object, (Executable) method, parameterValues, groups );
	}
}

Project source code

/layfoundation/spring-param-validate

appendix

-api (version 2.0.1) All annotations

annotation illustrate
@AssertFalse Verify that the boolean type value is false
@AssertTrue Verify that the boolean type value is true
@DecimalMax(value) Verify whether the size of the number is less than or equal to the specified value, and the decimal has accuracy
@DecimalMin(value) Verify whether the size of the number is greater than or equal to the specified value, and the decimal has accuracy
@Digits(integer, fraction) Verify that the number meets the specified format
@Email Verify that the string meets the format of the email address
@Future Verify that a date or time is after the current time
@FutureOrPresent Verify that a date or time is after or equal to the current time
@Max(value) Verify that the size of the number is less than or equal to the specified value
@Min(value) Verify that the size of the number is greater than or equal to the specified value
@Negative Verify that the number is a negative integer, 0 is invalid
@NegativeOrZero Verify that the number is a negative integer
@NotBlank Verification string cannot be empty null or "", it can only be used for string verification
@NotEmpty Verification object must not be empty and can be used for maps and arrays
@NotNull Verification object is not null
@Null The verification object must be null
@past Verify that a date or time is before the current time.
@PastOrPresent Verify that a date or time is before or equal to the current time.
@Pattern(value) Rules to verify whether a string complies with regular expressions
@Positive Verify that the number is a positive integer, 0 is invalid
@PositiveOrZero Verify that the number is a positive integer
@Size(max, min) Verify that the length of the object (string, collection, array) is within the specified range

Common annotations supplemented by hibernate-validator (version 6.0.)

annotation illustrate
@Length The commented string must be within the specified range
@Range The annotated element must be within the appropriate range
@SafeHtml The annotated element must be a safe Html
@URL The annotated element must be a valid URL

Summarize

The above is personal experience. I hope you can give you a reference and I hope you can support me more.