preface

In this article, we will cover best practices and implementation principles for Spring Validation in various scenarios. Project source: spring-validation

Simple to use

The Java API specification (JSR303) defines the standard validation-API for Bean validation, but does not provide an implementation. Hibernate Validation is an implementation of this specification with validation annotations such as @email, @Length, and so on. Spring Validation is a secondary encapsulation of Hibernate Validation to support automatic Validation of Spring MVC parameters. Next, use the Spring-Boot project as an example to illustrate the use of Spring Validation.

Introduction of depend on

If the spring-boot version is smaller than 2.3.x, spring-boot-starter-Web automatically passes in a Hibernate-Validator dependency. If the spring-Boot version is larger than 2.3.x, you need to manually import dependencies:

<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> < version > 6.0.1. Final < / version > < / dependency >Copy the code

For Web services, in order to prevent the impact of illegal parameters on services, parameter verification must be done in the Controller layer! In most cases, request parameters come in one of two forms:

1.POST and PUT requests, using requestBody to pass parameters;

2. GET requests, using requestParam/PathVariable passing parameters.

Here we introduces the requestBody and requestParam/PathVariable parameter calibration of actual combat!

RequestBody parameter verification

POST and PUT requests typically use requestBody to pass parameters, in which case the back end receives them using a DTO object. The automatic parameter verification can be achieved by adding the @Validated annotation to the DTO object. For example, if you have an interface to save the User, the length of userName should be 2-10, and the length of account and password should be 6-20. If validation fails, throws MethodArgumentNotValidException abnormalities, default will Spring into 400 (Bad Request) Request.

DTO represents a Data Transfer Object and is used for interactive transmission between the server and client. In a Spring-Web project, you can represent the Bean object used to receive the request parameters.

Declare the constraint annotation on the DTO field

@Data
public class UserDTO {

    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}
Copy the code

Declare validation annotations on method parameters

@postMapping ("/save") public Result saveUser(@requestBody @validated UserDTO UserDTO) { Return result.ok (); }Copy the code

So in this case, you can use either @Valid or @Validated.

RequestParam/PathVariable parameter calibration

GET requests are generally use requestParam/PathVariable refs. If the number of parameters is large (for example, more than 6), it is recommended to use DTO objects. Otherwise, it is recommended that each parameter be tiled into a method entry. In this case, you must annotate the Controller class with an @Validated annotation and declare a constraint annotation (such as @min, etc.) on the input parameter. If the check fails, throws ConstraintViolationException anomalies. A code example is as follows:

@requestMapping ("/ API /user") @restController @validated Public Class UserController {// Path variable @getMapping ("{userId}") Public Result detail(@pathVariable ("userId") @min (10000000000000000L) Long userId) { UserDTO UserDTO = new UserDTO(); userDTO.setUserId(userId); userDTO.setAccount("11111111111111111"); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); } @getMapping ("getByAccount") public Result getByAccount(@length (min = 6, Max = 20) @notnull String Account) {// The service logic will be processed only if the verification succeeds. userDTO.setUserId(10000000000000003L); userDTO.setAccount(account); userDTO.setUserName("xixi"); userDTO.setAccount("11111111111111111"); return Result.ok(userDTO); }}Copy the code

Unified Exception Handling

Said before, if the check fails, throws MethodArgumentNotValidException or ConstraintViolationException anomalies. In real project development, unified exception handling is often used to return a more friendly prompt. For example, our system requires that no matter what exceptions are sent, the HTTP status code must return 200, so that the business code can distinguish the exceptions in the system.

@RestControllerAdvice public class CommonExceptionHandler { @ExceptionHandler({MethodArgumentNotValidException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { BindingResult bindingResult = ex.getBindingResult(); StringBuilder sb = new StringBuilder(" check error :"); for (FieldError fieldError : BindingResult. GetFieldErrors ()) {sb. Append (fieldError. GetField ()), append (" : ").append(fieldError.getDefaultMessage()).append(", "); } String msg = sb.toString(); return Result.fail(BusinessCode. Parameter verification failed, MSG); } @ExceptionHandler({ConstraintViolationException.class}) @ResponseStatus(HttpStatus.OK) @ResponseBody public Result handleConstraintViolationException(ConstraintViolationException ex) { return Result.fail(BusinessCode. Parameter verification failed, ex.getMessage()); }}Copy the code

Use the advanced

Packet check

In a real project, multiple methods may need to use the same DTO class to receive parameters, and the validation rules for different methods may be different. At this point, simply adding constraint annotations to the fields of the DTO class will not solve the problem. Therefore, Spring-Validation supports grouping validation, which is specifically designed to solve this problem. The value of UserId must be >=10000000000000000L; The validation rules for other fields are the same in both cases. Example code for grouping verification at this time is as follows:

Constraint the group information groups declared applicable on the annotation

@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; Public interface Update {} public interface Update {} public interface Update {}Copy the code

Specify the verification groups on the @Validated annotation

@postMapping ("/save") public Result saveUser(@requestBody @validated (userTo.save.class) UserDTO UserDTO) { Return result.ok (); } @PostMapping("/update") public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) { // Return result.ok (); }Copy the code

Nested check

In the previous example, the fields in the DTO class were all primitive data types and strings. However, in real scenarios, a field may also be an object. In this case, nested verification can be used first. For example, the User information is stored with Job information. Note that the corresponding field of the DTO class must be marked with the @VALID annotation.

@Data public class UserDTO { @Min(value = 10000000000000000L, groups = Update.class) private Long userId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String userName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String account; @NotNull(groups = {Save.class, Update.class}) @Length(min = 6, max = 20, groups = {Save.class, Update.class}) private String password; @NotNull(groups = {Save.class, Update.class}) @Valid private Job job; @Data public static class Job { @Min(value = 1, groups = Update.class) private Long jobId; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String jobName; @NotNull(groups = {Save.class, Update.class}) @Length(min = 2, max = 10, groups = {Save.class, Update.class}) private String position; Public interface Update {} public interface Update {} public interface Update {}Copy the code

Nested checksum can be used in conjunction with grouping checksum. The nested set validates every item in the set. For example, the List field validates every Job object in the List.

A collection of check

If the request body passes a JSON array directly to the background, and you want to validate each item in the array. If we use a list or set under java.util.Collection to receive data, the validation will not take effect. We can use a custom list collection to receive arguments:

Wrap the List type and declare the @valid annotation

Public class ValidationList<E> implements List<E> {@delegate // @delegate is a Lombok annotation @valid // must add @valid annotation public List<E> list = new ArrayList<>(); @override public String toString() {return list.toString(); }}Copy the code

The @delegate annotation is limited by lombok versions and is supported later than 1.18.6. If the check is not passed, throws NotReadablePropertyException, also can use unified exception processing.

For example, if we need to save multiple User objects at once, the Controller layer method can be written like this:

@PostMapping("/saveList") public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> Return result.ok (); return result.ok (); }Copy the code

Custom check

Business requirements are always more complex than these simple validations provided by the framework, and we can customize the validations to meet our requirements. Customizing Spring Validation is very simple. Suppose we define a custom encryption ID (consisting of numbers or a-F letters, 32 to 256 characters in length), which consists of two steps:

Custom constraint annotations

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {EncryptIdValidator.class}) public @interface EncryptId {// Default error message String message() default "Encryption ID format error "; / / group Class <? >[] groups() default {}; / / load Class <? extends Payload>[] payload() default {}; }Copy the code

Implement the ConstraintValidator interface to write a ConstraintValidator

public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {private static final Pattern Pattern = pattern.compile ("^[a-f\\d]{32,256}$"); private static final Pattern Pattern = pattern.compile ("^[a-f\\d]{32,256}$"); @ Override public Boolean isValid (String value, ConstraintValidatorContext context) {/ / not null to check the if (the value! = null) { Matcher matcher = PATTERN.matcher(value); return matcher.find(); } return true; }}Copy the code

So we can use @encryptid to verify parameters!

Programmatic check

The examples above all implement automatic validation based on annotations, and in some cases we may want to invoke validation programmatically. This time can be injected javax.mail. Validation. The Validator object, then calls the API.

@Autowired private javax.validation.Validator globalValidator; @postMapping ("/saveWithCodingValidate") public Result saveWithCodingValidate(@requestBody UserDTO UserDTO) { Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class); // If the check succeeds, validate is null; If (validate.isempty ()) {// Validate, Will execute the business logic to handle} else {for (ConstraintViolation < UserDTO > userDTOConstraintViolation: Validate) {/ / calibration failure, do other logic System. Out. The println (userDTOConstraintViolation); } } return Result.ok(); }Copy the code

I Fail Fast.

By default, Spring Validation validates all fields before throwing an exception. You can perform some simple configurations to enable the Fali Fast mode and return immediately if the verification fails.

@Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure().failfast (true).buildValidatorFactory(); return validatorFactory.getValidator(); }Copy the code

“Valid” and “Validated”

Realize the principle of

Implementation principle of requestBody parameter verification

In spring MVC – RequestResponseBodyMethodProcessor is used to resolve parameters and processing @ @ RequestBody annotation ResponseBody tagging methods return values. The logic for checking arguments must be in the resolveArgument() method:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); / / request data encapsulation to DTO objects in the Object arg = readWithMessageConverters (webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory ! = null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg ! = null) {// Execute data validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer ! = null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); }}Copy the code

ResolveArgument () calls validateIfApplicable() for argument verification.

Protected void validateIfApplicable(WebDataBinder Binder, MethodParameter parameter) { Such as @ RequestBody, @ Valid, @ Validated the Annotation [] annotations = parameter. GetParameterAnnotations (); for (Annotation ann : Annotations) {/ / try to get the @ Validated annotation Validated validatedAnn = AnnotationUtils. GetAnnotation (Ann, Validated. Class); If @ "is specified, the verification function is enabled. // If not, check whether the argument is preceded by a Valid annotation. 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}); Validate (validationHints); break; }}}Copy the code

So if you look at this, you can see why the @, Validated and the @Valid annotations can be used intermixed. Let’s move on to the WebDatabinder.validate () implementation.

@Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator ! = null) {processConstraintViolations (/ / here calls Hibernate Validator perform real check this. TargetValidator. Validate (target, asValidationGroups(validationHints)), errors); }}Copy the code

It turns out that the bottom layer finally calls Hibernate Validator for the actual validation.

Method level parameter verification implementation principle

Method-level parameter validation refers to tiling parameters one by one into method parameters and then declaring constraint annotations in front of each parameter. In fact, this approach can be applied to any Spring Bean method, such as Controller/Service, etc. The underlying implementation principle is AOP, specifically by MethodValidationPostProcessor dynamically register AOP aspects, and then use the MethodValidationInterceptor woven into the tangent point methods to enhance.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {@override public void afterPropertiesSet() {// Create a Pointcut Pointcut for all '@' labeled beans. Pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); / / create the Advisor to enhance this. Advisor = new DefaultPointcutAdvisor (pointcut, createMethodValidationAdvice (enclosing the validator)); } // create Advice, Nature is a method interceptor protected Advice createMethodValidationAdvice (@ Nullable Validator Validator) {return (the Validator! = null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); }}Copy the code

Then look at the MethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation Invocation) throws Throwable {// Method without enhancement, Skip the if (isFactoryBeanMetadataMethod (invocation. GetMethod ())) {return invocation. The proceed (); } // get grouping information Class<? >[] groups = determineValidationGroups(invocation); ExecutableValidator execVal = this.validator.forExecutables(); Method methodToValidate = invocation.getMethod(); Set<ConstraintViolation<Object>> result; Try {// verify method entry, Eventually entrusted to Hibernate Validator to check the result. = execVal validateParameters (invocation. GetThis (), methodToValidate, invocation.getArguments(), groups); } catch (IllegalArgumentException ex) { ... } // If (! result.isEmpty()) { throw new ConstraintViolationException(result); } // The real method call Object returnValue = Invocation.proceed(); // Check the return value. Eventually entrusted to Hibernate Validator to check the result. = execVal validateReturnValue (invocation. GetThis (), methodToValidate, returnValue, groups); // If (! result.isEmpty()) { throw new ConstraintViolationException(result); } return returnValue; }}Copy the code

In fact, both requestBody parameter Validation and method-level Validation end up with Hibernate Validator being called for Validation, and Spring Validation is just a layer of encapsulation.

The last

I have arranged a: Spring related information documents and knowledge map, Java systematic information, (including Java core knowledge points, interview topics and the latest Internet real questions in 20 years, e-books, etc.) friends who need to pay attention to the public number [procedures Yuan Small wan] can obtain.