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<String> 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<Boolean> 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<List<UserVo>> 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<UserVo> list = new ArrayList<>(); 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<?>[] groups() default {}; Class<? extends Payload>[] 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<UserVo> { @Override public List<Class<?>> getValidationGroups(UserVo user) { List<Class<?>> defaultGroupSequence = new ArrayList<>(); (); 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<String> 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<? extends Annotation> 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 <U> boolean validateConstraintsForSingleDefaultGroupElement(BaseBeanValidationContext<?> validationContext, ValueContext<U, Object> valueContext, final Map<Class<?>, Class<?>> validatedInterfaces, Class<? super U> clazz, Set<MetaConstraint<?>> 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<?> metaConstraint : metaConstraints ) { final Class<?> declaringClass = ().getDeclaringClass(); if ( () ) { Class<?> validatedForClass = ( declaringClass ); if ( validatedForClass != null && !( clazz ) ) { continue; } ( declaringClass, clazz ); } boolean tmp = validateMetaConstraint( validationContext, valueContext, (), metaConstraint ); if ( shouldFailFast( validationContext ) ) { return false; } validationSuccessful = validationSuccessful && 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 |
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.